vxe-pc-ui
Version:
A vue based PC component library
417 lines (382 loc) • 12.8 kB
text/typescript
import { defineComponent, h, ref, Ref, nextTick, onBeforeUnmount, onMounted, reactive, watch, PropType } from 'vue'
import XEUtils from 'xe-utils'
import { getConfig, createEvent, useSize } from '../../ui'
import { getLastZIndex, nextZIndex } from '../../ui/src/utils'
import { getAbsolutePos, getDomNode } from '../../ui/src/dom'
import { getSlotVNs } from '../../ui/src/vn'
import type { VxeTooltipPropTypes, VxeTooltipConstructor, VxeTooltipEmits, TooltipInternalData, TooltipReactData, TooltipMethods, TooltipPrivateRef } from '../../../types'
export default defineComponent({
name: 'VxeTooltip',
props: {
modelValue: Boolean,
size: {
type: String as PropType<VxeTooltipPropTypes.Size>,
default: () => getConfig().tooltip.size || getConfig().size
},
selector: String as PropType<VxeTooltipPropTypes.Selector>,
trigger: {
type: String as PropType<VxeTooltipPropTypes.Trigger>,
default: () => getConfig().tooltip.trigger || 'hover'
},
theme: {
type: String as PropType<VxeTooltipPropTypes.Theme>,
default: () => getConfig().tooltip.theme || 'dark'
},
content: {
type: [String, Number] as PropType<VxeTooltipPropTypes.Content>,
default: null
},
useHTML: Boolean as PropType<VxeTooltipPropTypes.UseHTML>,
zIndex: [String, Number] as PropType<VxeTooltipPropTypes.ZIndex>,
popupClassName: [String, Function] as PropType<VxeTooltipPropTypes.PopupClassName>,
isArrow: {
type: Boolean as PropType<VxeTooltipPropTypes.IsArrow>,
default: () => getConfig().tooltip.isArrow
},
enterable: {
type: Boolean as PropType<VxeTooltipPropTypes.Enterable>,
default: () => getConfig().tooltip.enterable
},
enterDelay: {
type: Number as PropType<VxeTooltipPropTypes.EnterDelay>,
default: () => getConfig().tooltip.enterDelay
},
leaveDelay: {
type: Number as PropType<VxeTooltipPropTypes.LeaveDelay>,
default: () => getConfig().tooltip.leaveDelay
}
},
emits: [
'update:modelValue'
] as VxeTooltipEmits,
setup (props, context) {
const { slots, emit } = context
const xID = XEUtils.uniqueId()
const { computeSize } = useSize(props)
const reactData = reactive<TooltipReactData>({
target: null,
isUpdate: false,
visible: false,
tipContent: '',
tipActive: false,
tipTarget: null,
tipZindex: 0,
tipStore: {
style: {},
placement: '',
arrowStyle: {}
}
})
const internalData: TooltipInternalData = {
}
const refElem = ref() as Ref<HTMLDivElement>
const refMaps: TooltipPrivateRef = {
refElem
}
const $xeTooltip = {
xID,
props,
context,
reactData,
internalData,
getRefMaps: () => refMaps
} as unknown as VxeTooltipConstructor
let tooltipMethods = {} as TooltipMethods
const updateTipStyle = () => {
const { tipTarget, tipStore } = reactData
if (tipTarget) {
const { scrollTop, scrollLeft, visibleWidth } = getDomNode()
const { top, left } = getAbsolutePos(tipTarget)
const el = refElem.value
const marginSize = 6
const offsetHeight = el.offsetHeight
const offsetWidth = el.offsetWidth
let tipLeft = left
let tipTop = top - offsetHeight - marginSize
tipLeft = Math.max(marginSize, left + Math.floor((tipTarget.offsetWidth - offsetWidth) / 2))
if (tipLeft + offsetWidth + marginSize > scrollLeft + visibleWidth) {
tipLeft = scrollLeft + visibleWidth - offsetWidth - marginSize
}
if (top - offsetHeight < scrollTop + marginSize) {
tipStore.placement = 'bottom'
tipTop = top + tipTarget.offsetHeight + marginSize
}
tipStore.style.top = `${tipTop}px`
tipStore.style.left = `${tipLeft}px`
tipStore.arrowStyle.left = `${left - tipLeft + tipTarget.offsetWidth / 2}px`
}
}
const updateValue = (value: VxeTooltipPropTypes.ModelValue) => {
if (value !== reactData.visible) {
reactData.visible = value
reactData.isUpdate = true
emit('update:modelValue', value)
}
}
const updateZindex = () => {
if (reactData.tipZindex < getLastZIndex()) {
reactData.tipZindex = nextZIndex()
}
}
const clickEvent = () => {
if (reactData.visible) {
tooltipMethods.close()
} else {
handleVisible(reactData.target || getSelectorEl(), props.content)
}
}
const targetMouseenterEvent = () => {
handleVisible(reactData.target || getSelectorEl(), props.content)
}
const targetMouseleaveEvent = () => {
const { trigger, enterable, leaveDelay } = props
reactData.tipActive = false
if (enterable && trigger === 'hover') {
setTimeout(() => {
if (!reactData.tipActive) {
tooltipMethods.close()
}
}, leaveDelay)
} else {
tooltipMethods.close()
}
}
const wrapperMouseenterEvent = () => {
reactData.tipActive = true
}
const wrapperMouseleaveEvent = () => {
const { trigger, enterable, leaveDelay } = props
reactData.tipActive = false
if (enterable && trigger === 'hover') {
setTimeout(() => {
if (!reactData.tipActive) {
tooltipMethods.close()
}
}, leaveDelay)
}
}
const showTip = () => {
const { tipStore } = reactData
const el = refElem.value
if (el) {
const parentNode = el.parentNode
if (!parentNode) {
document.body.appendChild(el)
}
}
updateValue(true)
updateZindex()
tipStore.placement = 'top'
tipStore.style = { width: 'auto', left: 0, top: 0, zIndex: props.zIndex || reactData.tipZindex }
tipStore.arrowStyle = { left: '50%' }
return tooltipMethods.updatePlacement()
}
const handleDelayFn = () => {
internalData.showDelayTip = XEUtils.debounce(() => {
if (reactData.tipActive) {
showTip()
}
}, props.enterDelay, { leading: false, trailing: true })
}
const handleVisible = (target: HTMLElement | null, content?: VxeTooltipPropTypes.Content) => {
const contentSlot = slots.content
if (!contentSlot && (content === '' || XEUtils.eqNull(content))) {
return nextTick()
}
if (target) {
const { showDelayTip } = internalData
const { trigger, enterDelay } = props
reactData.tipActive = true
reactData.tipTarget = target
reactData.tipContent = content
if (enterDelay && trigger === 'hover') {
if (showDelayTip) {
showDelayTip()
}
} else {
return showTip()
}
}
return nextTick()
}
const getSelectorEl = () => {
const { selector } = props
if (selector) {
if (XEUtils.isElement(selector)) {
return selector as HTMLElement
}
if (XEUtils.isString(selector)) {
return document.querySelector(selector) as HTMLElement
}
}
return null
}
tooltipMethods = {
dispatchEvent (type, params, evnt) {
emit(type, createEvent(evnt, { $tooltip: $xeTooltip }, params))
},
open (target?: HTMLElement | null, content?: VxeTooltipPropTypes.Content) {
return handleVisible(target || reactData.target as HTMLElement || getSelectorEl(), content)
},
close () {
reactData.tipTarget = null
reactData.tipActive = false
Object.assign(reactData.tipStore, {
style: {},
placement: '',
arrowStyle: null
})
updateValue(false)
return nextTick()
},
toVisible (target: HTMLElement, content?: VxeTooltipPropTypes.Content) {
return handleVisible(target, content)
},
updatePlacement () {
return nextTick().then(() => {
const { tipTarget } = reactData
const el = refElem.value
if (tipTarget && el) {
updateTipStyle()
return nextTick().then(() => {
updateTipStyle()
})
}
})
},
isActived () {
return reactData.tipActive
},
setActived (active) {
reactData.tipActive = !!active
}
}
Object.assign($xeTooltip, tooltipMethods)
const renderContent = () => {
const { useHTML } = props
const { tipContent } = reactData
const contentSlot = slots.content
if (contentSlot) {
return h('div', {
key: 1,
class: 'vxe-table--tooltip-content'
}, getSlotVNs(contentSlot({})))
}
if (useHTML) {
return h('div', {
key: 2,
class: 'vxe-table--tooltip-content',
innerHTML: tipContent
})
}
return h('div', {
key: 3,
class: 'vxe-table--tooltip-content'
}, `${tipContent}`)
}
const renderVN = () => {
const { popupClassName, theme, isArrow, enterable } = props
const { tipActive, visible, tipStore } = reactData
const defaultSlot = slots.default
const vSize = computeSize.value
let ons
if (enterable) {
ons = {
onMouseenter: wrapperMouseenterEvent,
onMouseleave: wrapperMouseleaveEvent
}
}
return h('div', {
ref: refElem,
class: ['vxe-table--tooltip-wrapper', `theme--${theme}`, popupClassName ? (XEUtils.isFunction(popupClassName) ? popupClassName({ $tooltip: $xeTooltip }) : popupClassName) : '', {
[`size--${vSize}`]: vSize,
[`placement--${tipStore.placement}`]: tipStore.placement,
'is--enterable': enterable,
'is--visible': visible,
'is--arrow': isArrow,
'is--active': tipActive
}],
style: tipStore.style,
...ons
}, [
renderContent(),
h('div', {
class: 'vxe-table--tooltip-arrow',
style: tipStore.arrowStyle
}),
...(defaultSlot ? getSlotVNs(defaultSlot({})) : [])
])
}
watch(() => props.enterDelay, () => {
handleDelayFn()
})
watch(() => props.content, (val) => {
reactData.tipContent = val
})
watch(() => props.modelValue, (val) => {
if (!reactData.isUpdate) {
if (val) {
handleVisible(reactData.target || getSelectorEl(), props.content)
} else {
tooltipMethods.close()
}
}
reactData.isUpdate = false
})
onMounted(() => {
nextTick(() => {
const { trigger, content } = props
const wrapperElem = refElem.value
if (wrapperElem) {
const parentNode = wrapperElem.parentNode
if (parentNode) {
reactData.tipContent = content
reactData.tipZindex = nextZIndex()
XEUtils.arrayEach(wrapperElem.children, (elem, index) => {
if (index > 1) {
parentNode.insertBefore(elem, wrapperElem)
if (!reactData.target) {
reactData.target = elem as HTMLElement
}
}
})
parentNode.removeChild(wrapperElem)
const { target } = reactData
if (target) {
if (trigger === 'hover') {
target.onmouseenter = targetMouseenterEvent
target.onmouseleave = targetMouseleaveEvent
} else if (trigger === 'click') {
target.onclick = clickEvent
}
}
if (props.modelValue) {
handleVisible(target || getSelectorEl(), content)
}
}
}
})
})
onBeforeUnmount(() => {
const { target } = reactData
const wrapperElem = refElem.value
if (target) {
target.onmouseenter = null
target.onmouseleave = null
target.onclick = null
}
if (wrapperElem) {
const parentNode = wrapperElem.parentNode
if (parentNode) {
parentNode.removeChild(wrapperElem)
}
}
})
handleDelayFn()
$xeTooltip.renderVN = renderVN
return $xeTooltip
},
render () {
return this.renderVN()
}
})