本篇文章记录仿写一个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/、中文...

感兴趣的道友,能够闲暇工夫钻研钻研(像elementUIiviewBootstrapMaterial UI等都用到了proper.js)也是做的二次封装

另:prpper.js团队专门给react写了一套React Poppervue临时没有,所以咱们就学习vue-popper

本篇文章着眼于,中层底层原理vue-popper组件,让咱们开始学习吧

tooltip组件思考

什么是tooltip组件

  1. tooltip组件是用来做简略的文字附带阐明(提醒)的气泡框组件
  2. 个别交互是鼠标移入显示,鼠标移出隐没
  3. tooltip组件个别不会做简单的交互操作,以及承载过多的文本内容
  4. 能够了解为是dom元素title属性性能的具体补充

tooltip组件需要

  1. 暗黑模式tooltip,黑底白字
  2. 高亮模式tooltip,白底黑字
  3. tooltip组件的地位,在指向援用reference元素的那个方向,个别是上下左右,拓展共有12个方向
  4. tooltip的小三角形(个别是显示的)
  5. 可管制敞开开启,即符合条件hover展现,反之hover敞开
  6. 个别状况下tooltip都是单行内容,若内容过多,反对文字换行乃至自定义tooltip一些款式(反对插槽)
  7. 至于其余的需要如: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()函数

attrattribute单词属性的缩写,顾名思义,所以这个货色和属性无关

  • 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>// NPMnpm install vue-popperjs --save// Yarnyarn add vue-popperjs// Bowerbower 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>    &nbsp;&nbsp;&nbsp;    <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不同的属性和办法

墙裂倡议大家,看完当前,本人手写一下。只是看一遍,学习效果不太好