乐趣区

关于element-ui:elementui源码学习之仿写一个eltooltip

本篇文章记录仿写一个 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>
// 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>
    &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 不同的属性和办法

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

退出移动版