elementui源碼學(xué)習(xí)仿寫一個el-tooltip示例
前言
源碼在github上,大家可以拉下來,npm start運行跑起來,結(jié)合注釋有助于更好的理解。
github倉庫地址如下:
https://github.com/shuirongsh...
什么是編程
關(guān)于什么是編程這個問題,的確有很多答案。在很久以前,在筆者剛?cè)胄械臅r候,被告知了這樣一句話:
編程就是:規(guī)則的學(xué)習(xí),規(guī)則的使用,規(guī)則的理解、規(guī)則的自定義
有一定的道理...
背景介紹
我們在做組件的封裝時,常常會遇到一些“彈框組件”,以餓了么UI為例,比如:el-tooltip組件
、el-popover組件
、el-popconfirm組件
、el-dropdown組件
等,這類組件在操作的時候,常常會有一個彈框出現(xiàn),對于這些彈框的觸發(fā)條件(或懸浮、或點擊)以及位置的控制(上方、下方、左側(cè)、右側(cè))等,vue團隊專門封裝了一個vue-popper組件
,通過props
傳參以及一些事件方法的方式,去控制以達(dá)到我們想要的效果
那么,vue-popper組件
是如何實現(xiàn)的呢?底層原理是啥?是把popper.js
這個很優(yōu)秀的庫做了一層封裝
那么,popper.js
是如何實現(xiàn)的呢?底層原理是啥?是通過js
控制彈出框dom
的位置
由于popper.js
國內(nèi)資料不多,所以大家可以直接使用vue-popper組件
組件去做一些操作即可,畢竟其底層原理,也是prpper.js
el-tooltip組件
是使用了vue-popper組件
的規(guī)則vue-popper組件
是使用了popper.js庫
的規(guī)則popper.js庫
是使用了js和dom
的規(guī)則- 無限規(guī)則套娃...
附上傳送門
prpper.js 官網(wǎng):https://popper.js.org/、中文...
感興趣的道友,可以空閑時間研究研究(像elementUI
和iview
和Bootstrap
和Material UI
等都用到了proper.js
)也是做的二次封裝
另:prpper.js
團隊專門給react
寫了一套React Popper
,vue
暫時沒有,所以咱們就學(xué)習(xí)vue-popper
唄
本篇文章著眼于,中層底層原理vue-popper組件
,讓我們開始學(xué)習(xí)吧
tooltip組件思考
什么是tooltip組件
- tooltip組件是用來做簡單的文字附帶說明(提示)的氣泡框組件
- 一般交互是鼠標(biāo)移入顯示,鼠標(biāo)移出消失
- tooltip組件一般不會做復(fù)雜的交互操作,以及承載過多的文本內(nèi)容
- 可以理解為是dom元素title屬性功能的具體補充
tooltip組件需求
- 暗黑模式tooltip,黑底白字
- 高亮模式tooltip,白底黑字
- tooltip組件的位置,在指向引用reference元素的那個方向,一般是上下左右,拓展共有12個方向
- tooltip的小三角形(一般是顯示的)
- 可控制關(guān)閉開啟,即符合條件hover展示,反之hover關(guān)閉
- 一般情況下tooltip都是單行內(nèi)容,若內(nèi)容過多,支持文字換行乃至自定義tooltip一些樣式(支持插槽)
- 至于其他的需求如:tooltip顯示展開的過渡動畫、小箭頭是否可以隱藏、以及偏移量offset、延遲出現(xiàn)消失等,一般情況下不會怎么更改,所以本文著眼于重點常見需求,來進(jìn)行說明
在使用庫或者一些基礎(chǔ)組件之前,我們先嘗試一下,手寫一下
一個簡單的tooltip的demo
主要是使用屬性選擇器去控制,四個方向的tooltip和三角形小箭頭。
標(biāo)簽的whichPlacement屬性值為"top"時
,就讓其在上方,為left時
,就讓其在左側(cè),其他方位同理
demo效果圖
demo代碼
復(fù)制粘貼即可使用
<!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> <style> body { box-sizing: border-box; padding: 60px 240px; } /* 設(shè)置基本樣式 */ .item { width: fit-content; box-sizing: border-box; padding: 12px; border: 2px solid #aaa; /* 搭配偽元素,用相對定位 */ position: relative; } /* 使用偽元素創(chuàng)建tooltip */ .item::after { /* 內(nèi)容為 使用 tooltipContent的屬性值 */ content: attr(tooltipContent); position: absolute; background-color: #000; width: fit-content; height: auto; padding: 6px 12px; color: #fff; border-radius: 12px; /* 文字不換行 */ word-break: keep-all; display: none; } /* 使用偽元素創(chuàng)建小三角形 */ .item::before { content: ""; position: absolute; border-width: 6px 6px 0 6px; border-style: solid; border-color: transparent; border-top-color: black; display: none; } /* 上下左右四個方位,使用css的屬性選擇器控制tooltip和小三角形 */ /* 當(dāng)whichPlacement的屬性值為top時,做...樣式 */ /* 上方 */ [whichPlacement='top']::after { left: 50%; transform: translateX(-50%); top: -100%; } [whichPlacement='top']::before { top: -26%; left: 50%; transform: translateX(-50%); } /* 下方 */ /* 當(dāng)whichPlacement的屬性值為bottom時,做...樣式 */ /* 關(guān)于四個方向的小三角形,可以使用旋轉(zhuǎn)更改即可 */ [whichPlacement='bottom']::after { left: 50%; transform: translateX(-50%); bottom: -100%; } [whichPlacement='bottom']::before { bottom: -28%; left: 50%; transform: rotate(180deg); } /* 左側(cè) */ /* 當(dāng)whichPlacement的屬性值為left時,做...樣式 */ [whichPlacement='left']::after { top: 50%; transform: translateY(-50%); right: 108%; } [whichPlacement='left']::before { top: 50%; transform: translateY(-50%) rotate(270deg); left: -10.5px; } /* 右側(cè) */ /* 當(dāng)whichPlacement的屬性值為right時,做...樣式 */ [whichPlacement='right']::after { top: 50%; transform: translateY(-50%); left: 108%; } [whichPlacement='right']::before { top: 50%; transform: translateY(-50%) rotate(90deg); right: -10px; } .item:hover::after { display: block; } .item:hover::before { display: block; } </style> </head> <body> <div class="item" whichPlacement="top" tooltipContent="上方出現(xiàn)tooltip內(nèi)容">懸浮上方</div> <br> <div class="item" whichPlacement="bottom" tooltipContent="tooltip內(nèi)容在下方出現(xiàn)">懸浮下方</div> <br> <br> <br> <div class="item" whichPlacement="left" tooltipContent="左側(cè)出現(xiàn)tooltip內(nèi)容">懸浮左側(cè)</div> <br> <div class="item" whichPlacement="right" tooltipContent="tooltip內(nèi)容出現(xiàn)在右側(cè)">懸浮右側(cè)</div> </body> </html>
關(guān)于css屬性選擇器和attr()函數(shù)
上述代碼中用到了屬性選擇器和attr()函數(shù),這里簡單的提一下
屬性選擇器
問:什么是屬性選擇器?
答1:通過選取帶有指定標(biāo)簽屬性的dom元素,進(jìn)行樣式的設(shè)置
答2:通過標(biāo)簽的屬性名key和屬性值value來匹配元素,從而進(jìn)行樣式的設(shè)置
問:舉個例子唄
答:
[attr]
匹配所有具有attr
屬性的元素,不用管其值是什么,如:input[type]{ ... }
,意為:只要input
標(biāo)簽中,包含type
屬性(忽略type
屬性值),都選中,并設(shè)置...
樣式[attr='val']
匹配所有attr
屬性值等于val
,完全精準(zhǔn)匹配。如:input[type='text']{ ... }
,意為:只要input
標(biāo)簽中,有type
屬性,且屬性值為text
,才去選中,并匹配...
樣式[attr^='val']
匹配所有attr
屬性值以val
開頭的(上述demo案例中就用到了,只不過其屬性是我們自定義的)。模糊匹配[attr$='val']
,同上類似,^=
是以什么什么開頭匹配,$=
是以什么什么結(jié)尾匹配。模糊匹配[attr*='val']
,同上類似,*=
是只要包含即可,也是模糊匹配
詳見官方屬性選擇器介紹:https://www.w3school.com.cn/c...
attr()函數(shù)
attr
是attribute
單詞屬性
的縮寫,顧名思義,所以這個東西和屬性有關(guān)
css
的函數(shù)attr()
可獲取被選中元素的屬性值
,并且在樣式文件中使用??捎迷趥卧乩?,在偽類元素里使用,它得到的是偽元素的原始元素的值。attr()
函數(shù)可以和任何css
屬性一起使用,但是除了content
外,其余都還是試驗性的,所以建議:除了搭配偽元素的content
別的都不要用
如上述案例:
<div class="item" tooltipContent="上方出現(xiàn)tooltip內(nèi)容">懸浮上方</div> .item::after { /* 使用選中標(biāo)簽的tooltipContent屬性值作為content的內(nèi)容 */ content: attr(tooltipContent); }
官方attr函數(shù)介紹:https://developer.mozilla.org...
為什么要說屬性選擇器呢?因為封裝的代碼中能夠用到啊
使用vue-popper做組件的封裝
安裝
// CDN <script src="https://unpkg.com/@ckienle/k-pop"></script> // NPM npm install vue-popperjs --save // Yarn yarn add vue-popperjs // Bower bower install vue-popperjs --save
官方案例demo
<template> <popper trigger="clickToOpen" :options="{ placement: 'top', modifiers: { offset: { offset: '0,10px' } } }"> <div class="popper"> Popper Content </div> <button slot="reference"> Reference Element </button> </popper> </template> <script> import Popper from 'vue-popperjs'; import 'vue-popperjs/dist/vue-popper.css'; export default { components: { 'popper': Popper }, } </script>
官方demo效果圖
筆者的二次封裝效果圖
使用之代碼
下方代碼較多,建議打開編輯器,復(fù)制粘貼代碼,跑起來,閱讀之
<template> <div class="showTooltip"> <h3>暗色模式</h3> <br /> <div class="darkMode"> <div class="topBox"> <my-tooltip placement="top-start" content="top-start"> <span class="topReferenceDom">上方左側(cè)上方左側(cè)</span> </my-tooltip> <my-tooltip placement="top" content="top"> <span class="topReferenceDom">上方中間</span> </my-tooltip> <my-tooltip placement="top-end" content="top-end"> <span class="topReferenceDom">上方右側(cè)上方右側(cè)</span> </my-tooltip> </div> <div class="leftAndRightBox"> <div class="leftBox"> <my-tooltip placement="left-start" content="left-start"> <div class="leftReferenceDom">左側(cè)上方</div> </my-tooltip> <my-tooltip placement="left" content="left"> <div class="leftReferenceDom">左側(cè)中間</div> </my-tooltip> <my-tooltip placement="left-end" content="left-end"> <div class="leftReferenceDom">左側(cè)下方</div> </my-tooltip> </div> <div class="rightBox"> <my-tooltip placement="right-start" content="right-start"> <div class="rightReferenceDom">右側(cè)上方</div> </my-tooltip> <my-tooltip placement="right" content="right"> <div class="rightReferenceDom">右側(cè)中間</div> </my-tooltip> <my-tooltip placement="right-end" content="right-end"> <div class="rightReferenceDom">右側(cè)下方</div> </my-tooltip> </div> </div> <div class="bottomBox"> <my-tooltip placement="bottom-start" content="bottom-start"> <span class="bottomReferenceDom">下方左側(cè)下方左側(cè)</span> </my-tooltip> <my-tooltip placement="bottom" content="bottom"> <span class="bottomReferenceDom">下方中間</span> </my-tooltip> <my-tooltip placement="bottom-end" content="bottom-end"> <span class="bottomReferenceDom">下方右側(cè)下方右側(cè)</span> </my-tooltip> </div> </div> <br /> <h3>亮色模式</h3> <br /> <div class="lightMode"> <div class="topBox"> <my-tooltip light placement="top-start" content="top-start"> <span class="topReferenceDom">上方左側(cè)上方左側(cè)</span> </my-tooltip> <my-tooltip light placement="top" content="top"> <span class="topReferenceDom">上方中間</span> </my-tooltip> <my-tooltip light placement="top-end" content="top-end"> <span class="topReferenceDom">上方右側(cè)上方右側(cè)</span> </my-tooltip> </div> <div class="leftAndRightBox"> <div class="leftBox"> <my-tooltip light placement="left-start" content="left-start"> <div class="leftReferenceDom">左側(cè)上方</div> </my-tooltip> <my-tooltip light placement="left" content="left"> <div class="leftReferenceDom">左側(cè)中間</div> </my-tooltip> <my-tooltip light placement="left-end" content="left-end"> <div class="leftReferenceDom">左側(cè)下方</div> </my-tooltip> </div> <div class="rightBox"> <my-tooltip light placement="right-start" content="right-start"> <div class="rightReferenceDom">右側(cè)上方</div> </my-tooltip> <my-tooltip light placement="right" content="right"> <div class="rightReferenceDom">右側(cè)中間</div> </my-tooltip> <my-tooltip light placement="right-end" content="right-end"> <div class="rightReferenceDom">右側(cè)下方</div> </my-tooltip> </div> </div> <div class="bottomBox"> <my-tooltip light placement="bottom-start" content="bottom-start"> <span class="bottomReferenceDom">下方左側(cè)下方左側(cè)</span> </my-tooltip> <my-tooltip light placement="bottom" content="bottom"> <span class="bottomReferenceDom">下方中間</span> </my-tooltip> <my-tooltip light placement="bottom-end" content="bottom-end"> <span class="bottomReferenceDom">下方右側(cè)下方右側(cè)</span> </my-tooltip> </div> </div> <br /> <h3>可禁用</h3> <br /> <my-tooltip :disabled="disabled" placement="top" content="disabled屬性禁用"> <span class="item">懸浮出現(xiàn)</span> </my-tooltip> <button @click="disabled = !disabled">點擊啟用或禁用</button> <br /> <br /> <h3>當(dāng)tooltip內(nèi)容多的時候,使用content插槽</h3> <br /> <my-tooltip placement="top"> <span slot="content"> <div class="selfContent"> 內(nèi)容過多時,使用插槽更便于控制樣式,比如換行 </div> </span> <span class="item">懸浮出現(xiàn)</span> </my-tooltip> <br /> <br /> </div> </template> <script> export default { data() { return { disabled: false, }; }, }; </script> <style lang='less' scoped> .showTooltip { width: 100%; height: 100%; box-sizing: border-box; padding: 60px; padding-top: 0; padding-bottom: 120px; .topBox { .topReferenceDom { border: 1px solid #999; box-sizing: border-box; padding: 4px 8px; border-radius: 4px; width: 60px; text-align: center; margin-right: 6px; } } .leftAndRightBox { width: 100%; display: flex; padding-right: 120px; .leftBox { margin-right: 250px; } .leftReferenceDom, .rightReferenceDom { width: 72px; height: 60px; line-height: 60px; text-align: center; border: 1px solid #999; box-sizing: border-box; margin: 12px 0; } } .bottomBox { .bottomReferenceDom { border: 1px solid #999; box-sizing: border-box; padding: 4px 8px; border-radius: 4px; width: 60px; text-align: center; margin-right: 6px; } } .item { border: 1px solid #333; padding: 4px; } } .selfContent { width: 120px; color: #baf; font-weight: 700; } </style>
mytooltip封裝代碼
<template> <!-- 1. :appendToBody="true"是否把位置加到body外層標(biāo)簽上 餓了么UI和antD是true,iview和vuetifyjs是false 2. trigger屬性觸發(fā)方式,常用hover懸浮觸發(fā)、clickToOpen鼠標(biāo)點擊觸發(fā) 3. :visibleArrow="true"默認(rèn)顯示三角形小箭頭,但是可以修改 也可以使用偽元素自定義其對應(yīng)樣式,這樣更加自由靈活一些 4. :options="{ ... } 其實就是popper.js的配置項,可看對應(yīng)官方文檔 5. placement: placement 即為tooltip出現(xiàn)的位置,有12個位置,即:placementArr 6. modifiers: { ... } 此修飾符配置對象主要是控制定位的相關(guān)參數(shù) 7. offset即偏移量在原有位置上進(jìn)行移動微調(diào),這里暫時不設(shè)置了,直接使用 給.popper加上外邊距即可margin: 12px !important; 8. computeStyle.gpuAcceleration = false 關(guān)閉css3的transform定位,因為要自定義 9. preventOverflow.boundariesElement = 'window' 防止popper元素定位到邊界外 如:當(dāng)左側(cè)距離不夠用的時候,即使設(shè)置placement='left'但是tooltip依舊會在右側(cè) 10. <div class="popper" /> 此標(biāo)簽是tooltip的容器,所以我們可以設(shè)置對應(yīng)想要的樣式 11. rootClass="selfSetRootClass"搭配transition="fade"實現(xiàn)淡入淡出過渡效果 12. slot="reference"命名插槽是觸發(fā)tooltip打開/關(guān)閉的dom元素 13. disabled是否關(guān)閉這個tooltip --> <popper :appendToBody="true" trigger="hover" :visibleArrow="true" :options="{ placement: placement, modifiers: { offset: { offset: 0, }, computeStyle: { gpuAcceleration: false, }, preventOverflow: { boundariesElement: 'window', }, }, }" rootClass="selfSetRootClass" transition="fade" :disabled="disabled" > <!-- 內(nèi)容過多的時候,建議使用content插槽,便于自定義樣式 --> <div v-if="$slots.content" :class="{ isLightPopper: light }" ref="popperRef" class="popper" > <slot name="content"></slot> </div> <!-- 內(nèi)容少的話,直接content屬性 --> <div v-else :class="{ isLightPopper: light }" ref="popperRef" class="popper" > {{ content }} </div> <!-- 把外界傳遞的普通插槽當(dāng)做具名插槽傳遞給子組件使用 --> <slot slot="reference"></slot> </popper> </template> <script> // 基于vue-popperjs的二次封裝 import popper from "vue-popperjs"; // vue-popperjs基于popper.js二次封裝 import "vue-popperjs/dist/vue-popper.css"; // 總共12個位置 const placementArr = [ "top-start", "top", "top-end", "left-start", "left", "left-end", "right-start", "right", "right-end", "bottom-start", "bottom", "bottom-end", ]; export default { name: "myTooltip", components: { popper }, // 注冊并使用vue-popperjs插件組件 props: { // 12個tooltip位置 placement: { type: String, default: "top-start", // 默認(rèn) validator(val) { return placementArr.includes(val); // 位置校驗函數(shù) }, }, // 內(nèi)容(同內(nèi)容插槽,不過內(nèi)容插槽的權(quán)重高一些) content: { type: String, default: "", }, // 是否是亮色模式,默認(rèn)是暗色模式 light: { type: Boolean, default: false, }, // 是否禁用即關(guān)掉tooltip disabled: { type: Boolean, default: false, }, }, }; </script> <style lang="less"> // 覆蓋部分默認(rèn)的樣式(不用加/deep/ ) .popper { box-sizing: border-box; padding: 6px 12px; border-radius: 3px; color: #fff; background-color: #333; border: none; } // 設(shè)置一個tootip的外邊距(也可以使用offset) .popper[x-placement^="top"] { margin-bottom: 12px !important; } .popper[x-placement^="bottom"] { margin-top: 12px !important; } .popper[x-placement^="left"] { margin-right: 12px !important; } .popper[x-placement^="right"] { margin-left: 12px !important; } // 覆蓋原有的默認(rèn)三角形背景色樣式 .popper[x-placement^="top"] .popper__arrow { border-color: #333 transparent transparent transparent; } .popper[x-placement^="bottom"] .popper__arrow { border-color: transparent transparent #333 transparent; } .popper[x-placement^="right"] .popper__arrow { border-color: transparent #333 transparent transparent; } .popper[x-placement^="left"] .popper__arrow { border-color: transparent transparent transparent #333; } // 加上過渡效果(搭配transition="fade") .selfSetRootClass { transition: all 0.6s; } .fade-enter, .fade-leave-to { opacity: 0; } .fade-enter-active, .fade-leave-active { transition: opacity 0.6s; } // 亮色模式樣式 .isLightPopper { color: #333; background-color: #fff; filter: drop-shadow(0, 2px, 12px, 0, rgba(0, 0, 0, 0.24)); box-shadow: 0, 2px, 12px, 0, rgba(0, 0, 0, 0.24); } .isLightPopper[x-placement^="top"] .popper__arrow { border-color: #fff transparent transparent transparent; } .isLightPopper[x-placement^="bottom"] .popper__arrow { border-color: transparent transparent #fff transparent; } .isLightPopper[x-placement^="right"] .popper__arrow { border-color: transparent #fff transparent transparent; } .isLightPopper[x-placement^="left"] .popper__arrow { border-color: transparent transparent transparent #fff; } </style>
總結(jié)
因為mytooltip
組件,需要使用到的vue-popper
屬性和方法并不多,所以大家可以仿照筆者的方式,去看一下vue-popper
組件的代碼,然后結(jié)合自己公司的業(yè)務(wù)需求,去封裝一些適合自己公司的彈框組件
vue-popper:https://github.com/RobinCK/vu...
當(dāng)然,時間較為充裕的可以看一下popper.js
這個庫
關(guān)于vue-popper
組件的其他二次封裝的應(yīng)用,如封裝el-popover組件
、el-popconfirm組件
、el-dropdown組件
等,筆者會陸續(xù)更新的。不同的組件用到vue-popper
不同的屬性和方法
墻裂建議大家,看完以后,自己手寫一下。只是看一遍,學(xué)習(xí)效果不太好
以上就是elementui源碼學(xué)習(xí)仿寫一個el-tooltip示例的詳細(xì)內(nèi)容,更多關(guān)于elementui源碼仿寫el-tooltip的資料請關(guān)注腳本之家其它相關(guān)文章!