又是充满希望的一天!
前言
最近项目的各个模块及特殊操作需要增加名词解释,效果图如下,功能很简单,但现有的 Tooltip
组件却无法满足新需求,为此单独开发 IconTooltip
组件,并基于该组件进行 v-icon-tooltip
指令开发。
Todo list
- 指定元素后面追加名词解释图标
- 交互方式为单击切换提示信息
- 通过指令简易配置即可实现需求
IconTooltip
UI 框架使用的是 ViewDesign@4.5
版本,Tooltip
组件本身只支持悬停方式显示,但是可以通过提供的 disabled
和 always
来控制提示信息的显示时机。
// 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
组件,在封装组件时为了方便扩展,我们将 Tooltip
和 Icon
组件的一些属性暴露给外部调用,来方便其他特殊需求的使用。
v-icon-tooltip
在我当前的需求中,组件的图标是固定的,只是显示方式和内容不同,并且 IconTooltip
组件需要总是为其父元素设置 position
属性用来定位,此时在各个模块中去使用仍然比较繁琐。
对于一向以 懒人创造世界
为理念的我,决定再开发一个基于该组件的指令来满足特定的需求。
// 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]
}
指令使用
结尾
我是不会告诉你开始只是想从网上 copy
份指令改改方便偷懒,没想到后面就开发了一个完整组件。
写文章小白,若是阅读让你枯燥乏味请你边听摇滚边阅读,若是还有其他问题还请各位在评论区留言。
若能顺手点个小欣欣,鄙人感激不尽!Thanks♪(・ω・)ノ