本篇文章记录仿写一个
el-tooltip
组件细节,从而有助于大家更好了解饿了么 ui 对应组件具体工作细节。本文是 elementui 源码学习仿写系列的又一篇文章,后续闲暇了会不断更新并仿写其余组件。源码在 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
不同的属性和办法
墙裂倡议大家,看完当前,本人手写一下。只是看一遍,学习效果不太好