Vue 指令之 v-icon-tooltip 实现指南

又是充满但愿的一天!javascript

前言

最近项目的各个模块及特殊操做须要增长名词解释,效果图以下,功能很简单,但现有的 Tooltip 组件却没法知足新需求,为此单独开发 IconTooltip 组件,并基于该组件进行 v-icon-tooltip 指令开发。css

image.png

Todo list

  • 指定元素后面追加名词解释图标
  • 交互方式为单击切换提示信息
  • 经过指令简易配置便可实现需求

IconTooltip

UI 框架使用的是 ViewDesign@4.5 版本,Tooltip 组件自己只支持悬停方式显示,可是能够经过提供的 disabledalways 来控制提示信息的显示时机。html

// components/IconTooltip/icon-tooltip.vue
<template>
  <div ref="mainRef" class="icon-tooltip-wrapper" :style="wrapperStyles" :class="classes" @click.stop > <Tooltip v-bind="tooltipProps"> <Icon class="icon" v-bind="iconProps" v-click-outside:[capture]="onClickOutside" v-click-outside:[capture].mousedown="onClickOutside" v-click-outside:[capture].touchstart="onClickOutside" @click.native="onIconClick" /> </Tooltip> </div>
</template>

<script> import { defineProps } from '@/util' import { pick } from 'lodash' import { directive as ClickOutside } from '@/directives/v-click-outside' /** * Tooltip 触发类型 */ export const TRIGGER_TYPE = Object.freeze({ click: 'click', hover: 'hover' }) const ICON_PROP_KEYS = Object.freeze(['icon', 'custom', 'color', 'size']) const TOOLTIP_PROP_KEYS = Object.freeze([ 'content', 'placement', 'theme', 'maxWidth', 'transfer', 'disabled', 'always' ]) export default { name: 'IconTooltip', directives: { ClickOutside }, props: { icon: defineProps(String), custom: defineProps(String), color: defineProps(String), size: defineProps(Number, 16), content: defineProps(String, ''), triggerType: defineProps(String, TRIGGER_TYPE.click), placement: defineProps(String, 'top'), theme: defineProps(String, 'dark'), maxWidth: defineProps([String, Number], 200), transfer: defineProps(Boolean, true), capture: defineProps(Boolean, true), styles: defineProps(Object, null), classes: defineProps([String, Object, Array], null) }, data() { return { // 初始化时根据当前触发类型设置是否禁用 disabled: this.triggerType === TRIGGER_TYPE.click, // 单击后设为 true,可一直显示 Tooltip always: false } }, computed: { tooltipProps() { return pick(this, TOOLTIP_PROP_KEYS) }, iconProps() { const props = pick(this, ICON_PROP_KEYS) props.type = props.type || props.icon return props }, wrapperStyles() { return Object.assign( {}, // calc(100% + 6px) 是为了让元素以自身右边做为 x 轴的偏移起始坐标,6px 为偏移量 { top: '50%', right: 0, transform: 'translate(calc(100% + 6px), -50%)' }, this.styles ) } }, methods: { // 单击图标外部时关闭 tooltip onClickOutside() { if (this.triggerType === TRIGGER_TYPE.hover) return this.disabled = true this.always = false }, // 单击图标时切换 tooltip onIconClick() { if (this.triggerType === TRIGGER_TYPE.hover) return this.disabled = !this.disabled this.always = !this.always } } } </script>

<style lang='less' scoped> .icon-tooltip-wrapper { position: absolute; & .icon { cursor: pointer; } } </style>
复制代码

根据需求实现了须要的 IconTooltip 组件,在封装组件时为了方便扩展,咱们将 TooltipIcon 组件的一些属性暴露给外部调用,来方便其余特殊需求的使用。vue

v-icon-tooltip

在我当前的需求中,组件的图标是固定的,只是显示方式和内容不一样,而且 IconTooltip 组件须要老是为其父元素设置 position 属性用来定位,此时在各个模块中去使用仍然比较繁琐。java

对于一贯以 懒人创造世界 为理念的我,决定再开发一个基于该组件的指令来知足特定的需求。markdown

// directives/v-icon-tooltip.js
import { getStyle, upObjVal } from '@/util'
import Vue from 'vue'
import IconTooltip, { TRIGGER_TYPE } from '@/components/IconTooltip/icon-tooltip.vue'
import { get, omit } from 'lodash'

const PREFIX = '[v-icon-tooltip]'

const buildContent = (binding) =>
  (typeof binding.arg === 'string' ? binding.arg : '') || get(binding.value, 'content', '')

const buildProps = (...props) =>
  upObjVal(
    {
      icon: 'md-help-circle',
      custom: undefined, // icon props custom
      size: undefined,
      color: undefined,
      triggerType: TRIGGER_TYPE.click, // optional type: click, hover
      content: '',
      placement: 'top',
      theme: 'dark',
      maxWidth: 200,
      transfer: true,
      capture: true,
      styles: null,
      classes: null
    },
    ...props
  )

function buildTooltip(props) {
  // 经过 Vue.extend 获取组件的构造器
  const ctor = Vue.extend(IconTooltip)
  // 返回组件的实例化对象
  return new ctor({ propsData: props }).$mount()
}

function bindTooltip(el, binding) {
  if ('arg' in binding && typeof binding.arg !== 'string') {
    console.warn(`${PREFIX} Binding arg must be a string.`)
  }

  const $tooltip = buildTooltip(buildProps(binding.value, { content: buildContent(binding) }))

  el.$hasTooltip = true
  el.$tooltip = $tooltip

  el.appendChild($tooltip.$el)
}

export default {
  name: PREFIX,
  bind(el, binding) {
    bindTooltip(el, binding)
  },
  inserted(el) {
    // 获取指令挂载元素的 position 属性,这里的 getStyle 方法会获取元素包含类样式在内的 position 属性
    const rawPosition = getStyle(el, 'position') || ''

    // 若是原始的 position 属性能够进行定位则跳事后续步骤
    if (!['', 'static'].includes(rawPosition) || '$rawPosition' in el) return

    // 挂载原始定位属性并设置当前定位为相对定位
    el.$rawPosition = rawPosition
    el.style.position = 'relative'
  },
  update(el, binding) {
    if (el.$hasTooltip) {
      // 更新除 triggerType 的其余属性值
      return upObjVal(
        el.$tooltip._props,
        buildProps(omit(binding.value, 'triggerType'), { content: buildContent(binding) })
      )
    }
    bindTooltip(el, binding)
  },
  unbind(el) {
    // 指令销毁时销毁组件,若无这一步则在 tooltip 使用了 transfer 时,会产生垃圾元素
    el.$tooltip.$destroy()
    el.$tooltip = null

    // 还原元素的 position 值
    if ('$rawPosition' in el) {
      el.style.position = el.$rawPosition
    }

    // 逐一删除元素上的多余属性
    ;['$hasTooltip', '$tooltip', '$rawPosition'].forEach((key) => delete el[key])
  }
}
复制代码
// util/index.js
export const upObjVal = (target, ...sources) => {
  const onlySource = _.merge({}, ...sources)
  return _.merge(target, _.pick(onlySource, Object.keys(target)))
}

export function getStyle(el, attr, pseudo = null) {
  if (!(el instanceof HTMLElement)) {
    throw Error('The parameter el must be a HTMLElement.')
  }
  if (typeof attr !== 'string') {
    return ''
  }
  //IE6~8不兼容backgroundPosition写法,识别backgroundPositionX/Y
  if (attr === 'backgroundPosition' && !+'\v1') {
    return el.currentStyle.backgroundPositionX + ' ' + el.currentStyle.backgroundPositionY
  }
  const currentStyle = 'currentStyle' in el ? el.currentStyle : document.defaultView.getComputedStyle(el, pseudo)
  return currentStyle[attr]
}
复制代码

指令使用

image.png

image.png

结尾

我是不会告诉你开始只是想从网上 copy 份指令改改方便偷懒,没想到后面就开发了一个完整组件。app

image.png

写文章小白,如果阅读让你枯燥乏味请你边听摇滚边阅读,如果还有其余问题还请各位在评论区留言。框架

若能顺手点个小欣欣,鄙人感激涕零!Thanks♪(・ω・)ノless

相关文章
相关标签/搜索