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

相關文章
相關標籤/搜索