elementui源码学习仿写一个el-tooltip示例
作者:水冗水孚
前言
源码在github上,大家可以拉下来,npm start运行跑起来,结合注释有助于更好的理解。
github仓库地址如下:
https://github.com/shuirongsh...
什么是编程
关于什么是编程这个问题,的确有很多答案。在很久以前,在笔者刚入行的时候,被告知了这样一句话:
编程就是:规则的学习,规则的使用,规则的理解、规则的自定义
有一定的道理...
背景介绍
我们在做组件的封装时,常常会遇到一些“弹框组件”,以饿了么UI为例,比如:el-tooltip组件
、el-popover组件
、el-popconfirm组件
、el-dropdown组件
等,这类组件在操作的时候,常常会有一个弹框出现,对于这些弹框的触发条件(或悬浮、或点击)以及位置的控制(上方、下方、左侧、右侧)等,vue团队专门封装了一个vue-popper组件
,通过props
传参以及一些事件方法的方式,去控制以达到我们想要的效果
那么,vue-popper组件
是如何实现的呢?底层原理是啥?是把popper.js
这个很优秀的库做了一层封装
那么,popper.js
是如何实现的呢?底层原理是啥?是通过js
控制弹出框dom
的位置
由于popper.js
国内资料不多,所以大家可以直接使用vue-popper组件
组件去做一些操作即可,毕竟其底层原理,也是prpper.js
el-tooltip组件
是使用了vue-popper组件
的规则vue-popper组件
是使用了popper.js库
的规则popper.js库
是使用了js和dom
的规则- 无限规则套娃...
附上传送门
prpper.js 官网:https://popper.js.org/、中文...
感兴趣的道友,可以空闲时间研究研究(像elementUI
和iview
和Bootstrap
和Material UI
等都用到了proper.js
)也是做的二次封装
另:prpper.js
团队专门给react
写了一套React Popper
,vue
暂时没有,所以咱们就学习vue-popper
呗
本篇文章着眼于,中层底层原理vue-popper组件
,让我们开始学习吧
tooltip组件思考
什么是tooltip组件
- tooltip组件是用来做简单的文字附带说明(提示)的气泡框组件
- 一般交互是鼠标移入显示,鼠标移出消失
- tooltip组件一般不会做复杂的交互操作,以及承载过多的文本内容
- 可以理解为是dom元素title属性功能的具体补充
tooltip组件需求
- 暗黑模式tooltip,黑底白字
- 高亮模式tooltip,白底黑字
- tooltip组件的位置,在指向引用reference元素的那个方向,一般是上下左右,拓展共有12个方向
- tooltip的小三角形(一般是显示的)
- 可控制关闭开启,即符合条件hover展示,反之hover关闭
- 一般情况下tooltip都是单行内容,若内容过多,支持文字换行乃至自定义tooltip一些样式(支持插槽)
- 至于其他的需求如:tooltip显示展开的过渡动画、小箭头是否可以隐藏、以及偏移量offset、延迟出现消失等,一般情况下不会怎么更改,所以本文着眼于重点常见需求,来进行说明
在使用库或者一些基础组件之前,我们先尝试一下,手写一下
一个简单的tooltip的demo
主要是使用属性选择器去控制,四个方向的tooltip和三角形小箭头。
标签的whichPlacement属性值为"top"时
,就让其在上方,为left时
,就让其在左侧,其他方位同理
demo效果图
demo代码
复制粘贴即可使用
<!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; } /* 设置基本样式 */ .item { width: fit-content; box-sizing: border-box; padding: 12px; border: 2px solid #aaa; /* 搭配伪元素,用相对定位 */ position: relative; } /* 使用伪元素创建tooltip */ .item::after { /* 内容为 使用 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; } /* 使用伪元素创建小三角形 */ .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和小三角形 */ /* 当whichPlacement的属性值为top时,做...样式 */ /* 上方 */ [whichPlacement='top']::after { left: 50%; transform: translateX(-50%); top: -100%; } [whichPlacement='top']::before { top: -26%; left: 50%; transform: translateX(-50%); } /* 下方 */ /* 当whichPlacement的属性值为bottom时,做...样式 */ /* 关于四个方向的小三角形,可以使用旋转更改即可 */ [whichPlacement='bottom']::after { left: 50%; transform: translateX(-50%); bottom: -100%; } [whichPlacement='bottom']::before { bottom: -28%; left: 50%; transform: rotate(180deg); } /* 左侧 */ /* 当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; } /* 右侧 */ /* 当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="上方出现tooltip内容">悬浮上方</div> <br> <div class="item" whichPlacement="bottom" tooltipContent="tooltip内容在下方出现">悬浮下方</div> <br> <br> <br> <div class="item" whichPlacement="left" tooltipContent="左侧出现tooltip内容">悬浮左侧</div> <br> <div class="item" whichPlacement="right" tooltipContent="tooltip内容出现在右侧">悬浮右侧</div> </body> </html>
关于css属性选择器和attr()函数
上述代码中用到了属性选择器和attr()函数,这里简单的提一下
属性选择器
问:什么是属性选择器?
答1:通过选取带有指定标签属性的dom元素,进行样式的设置
答2:通过标签的属性名key和属性值value来匹配元素,从而进行样式的设置
问:举个例子呗
答:
[attr]
匹配所有具有attr
属性的元素,不用管其值是什么,如:input[type]{ ... }
,意为:只要input
标签中,包含type
属性(忽略type
属性值),都选中,并设置...
样式[attr='val']
匹配所有attr
属性值等于val
,完全精准匹配。如:input[type='text']{ ... }
,意为:只要input
标签中,有type
属性,且属性值为text
,才去选中,并匹配...
样式[attr^='val']
匹配所有attr
属性值以val
开头的(上述demo案例中就用到了,只不过其属性是我们自定义的)。模糊匹配[attr$='val']
,同上类似,^=
是以什么什么开头匹配,$=
是以什么什么结尾匹配。模糊匹配[attr*='val']
,同上类似,*=
是只要包含即可,也是模糊匹配
详见官方属性选择器介绍:https://www.w3school.com.cn/c...
attr()函数
attr
是attribute
单词属性
的缩写,顾名思义,所以这个东西和属性有关
css
的函数attr()
可获取被选中元素的属性值
,并且在样式文件中使用。可用在伪元素里,在伪类元素里使用,它得到的是伪元素的原始元素的值。attr()
函数可以和任何css
属性一起使用,但是除了content
外,其余都还是试验性的,所以建议:除了搭配伪元素的content
别的都不要用
如上述案例:
<div class="item" tooltipContent="上方出现tooltip内容">悬浮上方</div> .item::after { /* 使用选中标签的tooltipContent属性值作为content的内容 */ content: attr(tooltipContent); }
官方attr函数介绍: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效果图
笔者的二次封装效果图
使用之代码
下方代码较多,建议打开编辑器,复制粘贴代码,跑起来,阅读之
<template> <div class="showTooltip"> <h3>暗色模式</h3> <br /> <div class="darkMode"> <div class="topBox"> <my-tooltip placement="top-start" content="top-start"> <span class="topReferenceDom">上方左侧上方左侧</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">上方右侧上方右侧</span> </my-tooltip> </div> <div class="leftAndRightBox"> <div class="leftBox"> <my-tooltip placement="left-start" content="left-start"> <div class="leftReferenceDom">左侧上方</div> </my-tooltip> <my-tooltip placement="left" content="left"> <div class="leftReferenceDom">左侧中间</div> </my-tooltip> <my-tooltip placement="left-end" content="left-end"> <div class="leftReferenceDom">左侧下方</div> </my-tooltip> </div> <div class="rightBox"> <my-tooltip placement="right-start" content="right-start"> <div class="rightReferenceDom">右侧上方</div> </my-tooltip> <my-tooltip placement="right" content="right"> <div class="rightReferenceDom">右侧中间</div> </my-tooltip> <my-tooltip placement="right-end" content="right-end"> <div class="rightReferenceDom">右侧下方</div> </my-tooltip> </div> </div> <div class="bottomBox"> <my-tooltip placement="bottom-start" content="bottom-start"> <span class="bottomReferenceDom">下方左侧下方左侧</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">下方右侧下方右侧</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">上方左侧上方左侧</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">上方右侧上方右侧</span> </my-tooltip> </div> <div class="leftAndRightBox"> <div class="leftBox"> <my-tooltip light placement="left-start" content="left-start"> <div class="leftReferenceDom">左侧上方</div> </my-tooltip> <my-tooltip light placement="left" content="left"> <div class="leftReferenceDom">左侧中间</div> </my-tooltip> <my-tooltip light placement="left-end" content="left-end"> <div class="leftReferenceDom">左侧下方</div> </my-tooltip> </div> <div class="rightBox"> <my-tooltip light placement="right-start" content="right-start"> <div class="rightReferenceDom">右侧上方</div> </my-tooltip> <my-tooltip light placement="right" content="right"> <div class="rightReferenceDom">右侧中间</div> </my-tooltip> <my-tooltip light placement="right-end" content="right-end"> <div class="rightReferenceDom">右侧下方</div> </my-tooltip> </div> </div> <div class="bottomBox"> <my-tooltip light placement="bottom-start" content="bottom-start"> <span class="bottomReferenceDom">下方左侧下方左侧</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">下方右侧下方右侧</span> </my-tooltip> </div> </div> <br /> <h3>可禁用</h3> <br /> <my-tooltip :disabled="disabled" placement="top" content="disabled属性禁用"> <span class="item">悬浮出现</span> </my-tooltip> <button @click="disabled = !disabled">点击启用或禁用</button> <br /> <br /> <h3>当tooltip内容多的时候,使用content插槽</h3> <br /> <my-tooltip placement="top"> <span slot="content"> <div class="selfContent"> 内容过多时,使用插槽更便于控制样式,比如换行 </div> </span> <span class="item">悬浮出现</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外层标签上 饿了么UI和antD是true,iview和vuetifyjs是false 2. trigger属性触发方式,常用hover悬浮触发、clickToOpen鼠标点击触发 3. :visibleArrow="true"默认显示三角形小箭头,但是可以修改 也可以使用伪元素自定义其对应样式,这样更加自由灵活一些 4. :options="{ ... } 其实就是popper.js的配置项,可看对应官方文档 5. placement: placement 即为tooltip出现的位置,有12个位置,即:placementArr 6. modifiers: { ... } 此修饰符配置对象主要是控制定位的相关参数 7. offset即偏移量在原有位置上进行移动微调,这里暂时不设置了,直接使用 给.popper加上外边距即可margin: 12px !important; 8. computeStyle.gpuAcceleration = false 关闭css3的transform定位,因为要自定义 9. preventOverflow.boundariesElement = 'window' 防止popper元素定位到边界外 如:当左侧距离不够用的时候,即使设置placement='left'但是tooltip依旧会在右侧 10. <div class="popper" /> 此标签是tooltip的容器,所以我们可以设置对应想要的样式 11. rootClass="selfSetRootClass"搭配transition="fade"实现淡入淡出过渡效果 12. slot="reference"命名插槽是触发tooltip打开/关闭的dom元素 13. disabled是否关闭这个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" > <!-- 内容过多的时候,建议使用content插槽,便于自定义样式 --> <div v-if="$slots.content" :class="{ isLightPopper: light }" ref="popperRef" class="popper" > <slot name="content"></slot> </div> <!-- 内容少的话,直接content属性 --> <div v-else :class="{ isLightPopper: light }" ref="popperRef" class="popper" > {{ content }} </div> <!-- 把外界传递的普通插槽当做具名插槽传递给子组件使用 --> <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", // 默认 validator(val) { return placementArr.includes(val); // 位置校验函数 }, }, // 内容(同内容插槽,不过内容插槽的权重高一些) content: { type: String, default: "", }, // 是否是亮色模式,默认是暗色模式 light: { type: Boolean, default: false, }, // 是否禁用即关掉tooltip disabled: { type: Boolean, default: false, }, }, }; </script> <style lang="less"> // 覆盖部分默认的样式(不用加/deep/ ) .popper { box-sizing: border-box; padding: 6px 12px; border-radius: 3px; color: #fff; background-color: #333; border: none; } // 设置一个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; } // 覆盖原有的默认三角形背景色样式 .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>
总结
因为mytooltip
组件,需要使用到的vue-popper
属性和方法并不多,所以大家可以仿照笔者的方式,去看一下vue-popper
组件的代码,然后结合自己公司的业务需求,去封装一些适合自己公司的弹框组件
vue-popper:https://github.com/RobinCK/vu...
当然,时间较为充裕的可以看一下popper.js
这个库
关于vue-popper
组件的其他二次封装的应用,如封装el-popover组件
、el-popconfirm组件
、el-dropdown组件
等,笔者会陆续更新的。不同的组件用到vue-popper
不同的属性和方法
墙裂建议大家,看完以后,自己手写一下。只是看一遍,学习效果不太好
以上就是elementui源码学习仿写一个el-tooltip示例的详细内容,更多关于elementui源码仿写el-tooltip的资料请关注脚本之家其它相关文章!