100行代码超轻量 vue tooltip指令

1,029 阅读1分钟

C端没有引用任何第三方UI库,网上找没有合适的,就决定自己封装一个tooltip,有需要的话,用就完事了~ 示例地址

用法

import tooltip from '@/directives/tooltip';

export default {
    ...
    directives: {
      tooltip,
    },
    ...
}
<h1
  v-tooltip="{
    text: '嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿',
    placement: 'top',
  }"
>
  {{ msg }}
</h1>
<h1
  v-tooltip="{
    html: 'Hello !<br /><b>Vue</b> in CodeSandbox!',
    placement: 'top',
  }"
>
  xxxxx
</h1>

代码

/* eslint-disable max-len */
import Vue from 'vue';
import './tooltip.less';

// 提示框的id,每执行一次bind加1
let tooltipId = 1;

export default Vue.directive('vktooltip', {
  bind(el, binding) {
    const value = binding.value;
    const privateTooltipId = tooltipId;
    // 给提示框打标机,便于unbind的时候移除元素
    el.setAttribute('data-tooltip-id', privateTooltipId);
    // 处理入参
    const DEFAULT_BEHAVIER = 'hover';
    const DEFAULT_PLACEMENT = 'top';
    let text;
    let behavier;
    let placement;
    let html;
    if (typeof value === 'string') { // 传入为字符串,默认top、hover
      text = value;
      behavier = DEFAULT_BEHAVIER;
      placement = DEFAULT_PLACEMENT;
    } else {
      text = value.text;
      html = value.html;
      behavier = value.behavier ?? DEFAULT_BEHAVIER;
      placement = value.placement ?? DEFAULT_PLACEMENT;
    }

    let vktooltipContentDom;

    // 创建提示框dom元素,并添加到body
    const createVktooltipContentDom = () => {
      vktooltipContentDom = document.createElement('div');
      vktooltipContentDom.id = `tooltip_${privateTooltipId}`;
      if (html) {
        vktooltipContentDom.innerHTML = html;
      } else {
        vktooltipContentDom.innerText = text;
      }
      document.body.appendChild(vktooltipContentDom);
    };

    if (behavier === 'hover') {
      if (!el.getBoundingClientRect) {
        // 不支持getBoundingClientRect的浏览器退化成title属性
        el.setAttribute('title', text ?? html);
      } else {
        el.addEventListener('mouseenter', () => {
          const rect = el.getBoundingClientRect();
          if (!vktooltipContentDom) {
            createVktooltipContentDom();
          }
          const scrollY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
          vktooltipContentDom.style.display = 'block';
          vktooltipContentDom.className = 'vktooltip-content active';
          const setPosition = {
            top: () => {
              // 检测是否在危险区
              if (rect.y < vktooltipContentDom.clientHeight) {
                setPosition.bottom();
                return;
              }
              vktooltipContentDom.classList.add('vktooltip_top');
              vktooltipContentDom.style.left = `${rect.x - (vktooltipContentDom.clientWidth / 2) + (rect.width / 2)}px`;
              vktooltipContentDom.style.top = `${rect.y + scrollY - vktooltipContentDom.clientHeight - 10}px`;
            },
            bottom: () => {
              // 检测是否在危险区
              const viewPortHeight = document.documentElement.clientHeight || document.body.clientHeight;
              if (viewPortHeight < rect.y + rect.height + vktooltipContentDom.clientHeight) {
                setPosition.top();
                return;
              }
              vktooltipContentDom.classList.add('vktooltip_bottom');
              vktooltipContentDom.style.left = `${rect.x - (vktooltipContentDom.clientWidth / 2) + (rect.width / 2)}px`;
              vktooltipContentDom.style.top = `${rect.y + scrollY + rect.height + 10}px`;
            },
          };
          setPosition[placement]();
        });
        el.addEventListener('mouseout', () => {
          vktooltipContentDom.classList.remove('active');
          vktooltipContentDom.classList.add('fade-out');
        });
      }
    }
    tooltipId += 1;
  },
  unbind(el) {
    if (!el.getBoundingClientRect) {
      return;
    }
    const priviteTooltipId = el.getAttribute('data-tooltip-id');
    const tooltip = document.getElementById(`tooltip_${priviteTooltipId}`);
    if (tooltip) {
      document.body.removeChild(tooltip);
    }
  },
});

.vktooltip-content {
  position: absolute;
  max-width: 240px;
  padding: 6px 10px;
  line-height: 1.7;
  border-radius: 5px;
  font-size: 12px;
  background: #333;
  color: #fff;
  box-shadow: 0 5px 10px -5px rgba(0, 0, 0, 0.35);
  z-index: 1000;
  user-select: none;
  pointer-events: none;
  text-transform: none;
  &::before {
    content: ' ';
    border: 5px solid transparent;
    z-index: 1001;
    position: absolute;
    left: 50%;
  }

  &.vktooltip_bottom {
    &::before {
      top: 0;
      transform: translate(-50%, -100%);
      border-top: transparent;
      border-bottom-color: #333;
    }
  }
  &.vktooltip_top {
    &::before {
      bottom: 0;
      transform: translate(-50%, 100%);
      border-bottom: transparent;
      border-top-color: #333;
    }
  }
}
/* KEYFRAMES */
@keyframes tooltips-vert {
  0% {
    opacity: .9;
    transform: translate(50%, 0);
  }
  100% {
    opacity: .9;
    transform: translate(0, 0);
  }
}

@keyframes tooltips-horz {
  0% {
    opacity: 0;
    transform: translate(0, 10px);
  }
  100% {
    opacity: .9;
    transform: translate(0, 0);
  }
}

@keyframes tooltips-horz-reverse {
  0% {
    opacity: .9;
    transform: translate(0, 0);
  }

  100% {
    opacity: 0;
    transform: translate(0, 10px);
  }
}

.vktooltip-content.active {
  display: block;
}

// .vktooltip-content.vktooltip_left,
// .vktooltip-content.vktooltip_right {
//   &.active {
//     animation: tooltips-vert 200ms ease-out forwards;
//   }
// }

.vktooltip-content.vktooltip_top,
.vktooltip-content.vktooltip_bottom {
  &.fade-out {
    animation: tooltips-horz-reverse 200ms ease-in forwards;
    display: none;
  }
  &.active {
    animation: tooltips-horz 200ms ease-out forwards;
  }
}