3
This article records and imitates a el-tooltip component details, which will help you better understand the specific working details of the corresponding components of Ele.me ui. This article is another article in the elementui source code learning and imitation series. I will continue to update and imitate other components when I am free. The source code is on github, you can pull it down, npm start to run, and comments are helpful for better understanding. The github repository address is as follows:
https://github.com/shuirongshuifu/elementSrcCodeStudy

foreword

what is programming

There are indeed many answers to the question of what programming is. A long time ago, when I first entered the industry, I was told this sentence:

编程就是:规则的学习,规则的使用,规则的理解、规则的自定义

Makes sense...

Background introduction

When we are packaging components, we often encounter some "pop-up components", taking Ele.me UI as an example, such as: el-tooltip组件 , el-popover组件 , el-popconfirm组件 , el-dropdown组件 , etc. When this type of component is operating, a pop-up box will often appear, for the trigger conditions (or suspension, or click) of these pop-up boxes and the control of the position (above, below , left, right), etc., the vue team specially encapsulated a vue-popper组件 , through props pass parameters and some event methods to control to achieve the effect we want

So, how is vue-popper组件 implemented? What is the underlying principle? It is to popper.js this excellent library

So, how is popper.js implemented? What is the underlying principle? is controlled by js 弹出框dom the location of ---0e7a9841e2b3962dec24f0b5bb3ae89d---

Since popper.js there is not much domestic information, you can directly use the vue-popper组件 component to do some operations. After all, its underlying principle is also prpper.js

  • el-tooltip组件 is using the rule of vue-popper组件
  • vue-popper组件 is using the rules of popper.js库
  • popper.js库 is the rule that uses js和dom
  • Infinite Rules Matryoshka...

Attach the portal

prpper.js Official website: https://popper.js.org/, Chinese tutorial: https://www.jiyik.com/w/popperjs, good article: https://blog.csdn.net /jhzhahuaiyu/article/details/90213582

If you are interested, you can do research in your spare time (such as elementUI and iview and Bootstrap proper.js Material UI proper.js ) is also a secondary package

另: prpper.js团队reactReact Poppervue暂时没有,所以咱们就学习vue-popper chant

This article focuses on the underlying principles of the middle layer vue-popper组件 , let's start learning

Tooltip component thinking

What is a tooltip component

  1. The tooltip component is a bubble box component used to make simple text with instructions (hints).
  2. The general interaction is that the mouse moves into the display, and the mouse moves out to disappear.
  3. Tooltip components generally do not do complex interactive operations and carry too much text content
  4. It can be understood as a specific supplement to the title attribute function of the dom element

tooltip component requirements

  1. Dark mode tooltip, white text on black background
  2. Highlight mode tooltip, black text on white background
  3. The position of the tooltip component, in the direction pointing to the reference element, is generally up, down, left and right, and there are 12 directions for expansion.
  4. The small triangle of the tooltip (usually displayed)
  5. It can be controlled to close and open, that is, the hover display meets the conditions, otherwise the hover is closed
  6. In general, the tooltip is a single line of content. If there is too much content, it supports text wrapping and even custom tooltip styles (supports slots)
  7. As for other requirements, such as: tooltip shows the expanded transition animation, whether the small arrow can be hidden, offset offset, delay appearing and disappearing, etc., it will not be changed much under normal circumstances, so this article focuses on the key common requirements to explain

Before using the library or some basic components, let's try it out and write it by hand

A simple tooltip demo

Mainly use attribute selectors to control, tooltips in four directions and small triangular arrows.

The label's whichPlacement属性值为"top"时 , let it be on the top, it is left时 , let it be on the left, and the same is true for other directions

demo renderings

demo code

Copy and paste to use

 <!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>

About CSS attribute selectors and the attr() function

The attribute selector and attr() function are used in the above code, which is briefly mentioned here

attribute selector

Q: What is an attribute selector?

Answer 1: 通过选取带有指定标签属性的dom元素,进行样式的设置

Answer 2: 通过标签的属性名key和属性值value来匹配元素,从而进行样式的设置

Q: Give an example

answer:

  • [attr] attr ,不用管其值是什么,如: input[type]{ ... } ,意为: input标签, including the type attribute (ignoring type attribute value), select them, and set ... style
  • [attr='val'] match all attr attribute value equal to val , exact match.如: input[type='text']{ ... } ,意为:只要input标签中,有typetext ,才去选中, and match ... style
  • [attr^='val'] match all attr attribute values starting with val (used in the demo case above, but its attributes are custom). fuzzy matching
  • [attr$='val'] , similar to above, ^= matches what starts with what, $= matches what ends. fuzzy matching
  • [attr*='val'] , similar to the above, *= is a fuzzy match as long as it is included
For details, please refer to the introduction of the official attribute selector: https://www.w3school.com.cn/css/css_attribute_selectors.asp

attr() function

attr is attribute abbreviation for the word 属性 , as the name suggests, so this thing is related to attributes

  • The function of css attr() can get the 属性值 of the selected element, and use it in the style file. Can be used in pseudo-elements, used in pseudo-class elements, it gets the value of the original element of the pseudo-element.
  • attr() function can be used with any css property, but except content , the rest are experimental, so it is recommended: except with pseudo elements content don't use anything else

As in the above case:

 <div class="item" tooltipContent="上方出现tooltip内容">悬浮上方</div>

.item::after {
    /* 使用选中标签的tooltipContent属性值作为content的内容 */
    content: attr(tooltipContent);
}
Official attr function introduction: https://developer.mozilla.org/zh-CN/docs/Web/CSS/attr

Why do you say attribute selectors? Because it can be used in the encapsulated code.

Use vue-popper to package components

Install

 // 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

Official case 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>

Official demo renderings

The author's secondary packaging renderings

code to use

There is a lot of code below, it is recommended to open the editor, copy and paste the code, run it, and read it

 <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 Package code

 <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>

Summarize

Because the mytooltip component needs to use the vue-popper attributes and methods are not many, so you can follow the author's way and take a look at the code of the vue-popper component, Then combine with your company's business needs to package some suitable for your company 弹框组件

vue-popper : https://github.com/RobinCK/vue-popper

Of course, if you have more time, you can take a look at this library popper.js

Regarding the application of other secondary packaging of vue-popper components, such as packaging el-popover组件 , el-popconfirm组件 , el-dropdown组件 etc. will be updated successively. Different components use vue-popper different properties and methods

The wall crack suggests that everyone, after reading it, write it by hand. Just watch it once, the learning effect is not very good

水冗水孚
1.1k 声望584 粉丝

每一个不曾起舞的日子,都是对生命的辜负