vxe-pc-ui
Version:
A vue based PC component library
381 lines (380 loc) • 14.1 kB
JavaScript
import { defineComponent, h, ref, nextTick, onBeforeUnmount, onMounted, reactive, watch } 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';
export default defineComponent({
name: 'VxeTooltip',
props: {
modelValue: Boolean,
size: {
type: String,
default: () => getConfig().tooltip.size || getConfig().size
},
selector: String,
trigger: {
type: String,
default: () => getConfig().tooltip.trigger || 'hover'
},
theme: {
type: String,
default: () => getConfig().tooltip.theme || 'dark'
},
content: {
type: [String, Number],
default: null
},
useHTML: Boolean,
zIndex: [String, Number],
popupClassName: [String, Function],
isArrow: {
type: Boolean,
default: () => getConfig().tooltip.isArrow
},
enterable: {
type: Boolean,
default: () => getConfig().tooltip.enterable
},
enterDelay: {
type: Number,
default: () => getConfig().tooltip.enterDelay
},
leaveDelay: {
type: Number,
default: () => getConfig().tooltip.leaveDelay
}
},
emits: [
'update:modelValue'
],
setup(props, context) {
const { slots, emit } = context;
const xID = XEUtils.uniqueId();
const { computeSize } = useSize(props);
const reactData = reactive({
target: null,
isUpdate: false,
visible: false,
tipContent: '',
tipActive: false,
tipTarget: null,
tipZindex: 0,
tipStore: {
style: {},
placement: '',
arrowStyle: {}
}
});
const internalData = {};
const refElem = ref();
const refMaps = {
refElem
};
const $xeTooltip = {
xID,
props,
context,
reactData,
internalData,
getRefMaps: () => refMaps
};
let 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) => {
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, 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;
}
if (XEUtils.isString(selector)) {
return document.querySelector(selector);
}
}
return null;
};
tooltipMethods = {
dispatchEvent(type, params, evnt) {
emit(type, createEvent(evnt, { $tooltip: $xeTooltip }, params));
},
open(target, content) {
return handleVisible(target || reactData.target || getSelectorEl(), content);
},
close() {
reactData.tipTarget = null;
reactData.tipActive = false;
Object.assign(reactData.tipStore, {
style: {},
placement: '',
arrowStyle: null
});
updateValue(false);
return nextTick();
},
toVisible(target, 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-tooltip--content'
}, getSlotVNs(contentSlot({})));
}
if (useHTML) {
return h('div', {
key: 2,
class: 'vxe-tooltip--content',
innerHTML: tipContent
});
}
return h('div', {
key: 3,
class: 'vxe-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', Object.assign({ ref: refElem, class: ['vxe-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-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;
}
}
});
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();
}
});