v-tooltip自定义指令封装
前言
前端开发中,经常遇到文字太长盒子装不下的问题,我们一般都是超出以省略号展示,鼠标悬浮展示提示框,提示框中展示全部的文字;但是如果不超出不展示省略号的时候,鼠标悬浮不展示提示框是一个不太好处理的问题。今天我来分享一个vue中的自定义指令封装代码,可以很好地解决这个问题。
代码
javascript
/*
* @Description: 文字溢出提示指令,宽度不足时显示省略号并在悬浮时展示完整内容
* @FilePath: /src/directives/tooltip/index.ts
*
* 用法:
* v-tooltip 取元素自身文本作为提示内容
* v-tooltip="content" 指定提示内容
* v-tooltip="{ content, placement, effect, lines, disabled, maxWidth }"
* v-tooltip.bottom="content" 通过修饰符指定弹出方向
*/
import { DirectiveOptions } from 'vue';
type Placement = 'top' | 'bottom' | 'left' | 'right';
interface ITooltipOptions {
content?: string;
placement?: Placement;
effect?: 'dark' | 'light';
lines?: number;
disabled?: boolean;
maxWidth?: number;
}
interface ITooltipEl extends HTMLElement {
__tooltip__?: {
options: ITooltipOptions;
onEnter: () => void;
onLeave: () => void;
};
}
const POPPER_CLASS = 'v-tooltip-popper';
const STYLE_ID = 'v-tooltip-style';
const PLACEMENTS: Placement[] = ['top', 'bottom', 'left', 'right'];
// 提示框与元素的间距、提示框与视口边缘的最小留白
const GAP = 10;
const EDGE = 8;
let popper: HTMLElement | null = null;
let arrow: HTMLElement | null = null;
let activeEl: HTMLElement | null = null;
function injectStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
.${POPPER_CLASS} {
position: fixed;
top: 0;
left: 0;
z-index: 9999;
white-space: normal;
word-break: break-all;
line-height: 1.4;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.${POPPER_CLASS}.is-visible {
opacity: 1;
}`;
document.head.appendChild(style);
}
function getPopper(): HTMLElement {
if (!popper) {
injectStyle();
popper = document.createElement('div');
popper.className = `el-tooltip__popper ${POPPER_CLASS}`;
arrow = document.createElement('div');
arrow.className = 'popper__arrow';
popper.appendChild(arrow);
document.body.appendChild(popper);
}
return popper;
}
function normalizeOptions(value: any, modifiers: { [key: string]: boolean }): ITooltipOptions {
const options: ITooltipOptions =
value && typeof value === 'object'
? { ...value }
: { content: value === undefined || value === null ? '' : String(value) };
const modifierPlacement = PLACEMENTS.find(item => modifiers[item]);
if (modifierPlacement) {
options.placement = modifierPlacement;
}
return options;
}
// 设置省略号样式,单行省略或多行截断
function applyEllipsis(el: HTMLElement, lines: number) {
if (lines > 1) {
el.style.display = '-webkit-box';
el.style.webkitBoxOrient = 'vertical';
el.style.webkitLineClamp = String(lines);
el.style.whiteSpace = 'normal';
el.style.overflow = 'hidden';
} else {
el.style.whiteSpace = 'nowrap';
el.style.overflow = 'hidden';
el.style.textOverflow = 'ellipsis';
}
}
// 内容是否超出容器,未超出时不显示提示框
function isOverflow(el: HTMLElement, lines: number): boolean {
return lines > 1 ? el.scrollHeight - el.clientHeight > 1 : el.scrollWidth - el.clientWidth > 1;
}
function getContent(el: HTMLElement, options: ITooltipOptions): string {
if (options.content) return options.content;
return (el.textContent || '').trim();
}
function updatePosition(el: HTMLElement, placement: Placement) {
const box = getPopper();
const rect = el.getBoundingClientRect();
const width = box.offsetWidth;
const height = box.offsetHeight;
const { clientWidth: vw, clientHeight: vh } = document.documentElement;
// 空间不足时自动翻转方向
let finalPlacement = placement;
if (placement === 'top' && rect.top - height - GAP < EDGE) finalPlacement = 'bottom';
else if (placement === 'bottom' && rect.bottom + height + GAP > vh - EDGE) finalPlacement = 'top';
else if (placement === 'left' && rect.left - width - GAP < EDGE) finalPlacement = 'right';
else if (placement === 'right' && rect.right + width + GAP > vw - EDGE) finalPlacement = 'left';
let left = 0;
let top = 0;
if (finalPlacement === 'top' || finalPlacement === 'bottom') {
left = rect.left + rect.width / 2 - width / 2;
top = finalPlacement === 'top' ? rect.top - height - GAP : rect.bottom + GAP;
} else {
left = finalPlacement === 'left' ? rect.left - width - GAP : rect.right + GAP;
top = rect.top + rect.height / 2 - height / 2;
}
left = Math.min(Math.max(left, EDGE), Math.max(vw - width - EDGE, EDGE));
top = Math.min(Math.max(top, EDGE), Math.max(vh - height - EDGE, EDGE));
box.setAttribute('x-placement', finalPlacement);
box.style.transform = `translate(${Math.round(left)}px, ${Math.round(top)}px)`;
// 箭头跟随元素中心
if (arrow) {
if (finalPlacement === 'top' || finalPlacement === 'bottom') {
const offset = Math.min(Math.max(rect.left + rect.width / 2 - left, 12), Math.max(width - 12, 12));
arrow.style.left = `${Math.round(offset)}px`;
arrow.style.top = '';
} else {
const offset = Math.min(Math.max(rect.top + rect.height / 2 - top, 12), Math.max(height - 12, 12));
arrow.style.top = `${Math.round(offset)}px`;
arrow.style.left = '';
}
}
}
function hide() {
activeEl = null;
if (!popper) return;
popper.classList.remove('is-visible');
popper.style.display = 'none';
window.removeEventListener('scroll', hide, true);
window.removeEventListener('resize', hide);
}
function show(el: HTMLElement, options: ITooltipOptions) {
const lines = Number(options.lines) || 1;
if (options.disabled || !isOverflow(el, lines)) return;
const content = getContent(el, options);
if (!content) return;
const box = getPopper();
box.className = `el-tooltip__popper ${POPPER_CLASS} ${options.effect === 'light' ? 'is-light' : 'is-dark'}`;
box.style.maxWidth = `${Number(options.maxWidth) || 400}px`;
box.style.display = 'block';
// 文本节点放在箭头之前,避免覆盖箭头元素
box.textContent = content;
if (arrow) box.appendChild(arrow);
activeEl = el;
updatePosition(el, options.placement || 'top');
// 定位完成后再淡入,避免出现位移
window.requestAnimationFrame(() => {
if (activeEl === el && popper) popper.classList.add('is-visible');
});
window.addEventListener('scroll', hide, true);
window.addEventListener('resize', hide);
}
export const tooltip: DirectiveOptions = {
inserted(el, binding) {
const target = el as ITooltipEl;
const options = normalizeOptions(binding.value, binding.modifiers);
applyEllipsis(target, Number(options.lines) || 1);
const onEnter = () => show(target, target.__tooltip__ ? target.__tooltip__.options : options);
const onLeave = () => {
if (activeEl === target) hide();
};
target.__tooltip__ = { options, onEnter, onLeave };
target.addEventListener('mouseenter', onEnter);
target.addEventListener('mouseleave', onLeave);
},
componentUpdated(el, binding) {
const target = el as ITooltipEl;
if (!target.__tooltip__) return;
const options = normalizeOptions(binding.value, binding.modifiers);
target.__tooltip__.options = options;
applyEllipsis(target, Number(options.lines) || 1);
if (activeEl === target) hide();
},
unbind(el) {
const target = el as ITooltipEl;
if (activeEl === target) hide();
if (target.__tooltip__) {
target.removeEventListener('mouseenter', target.__tooltip__.onEnter);
target.removeEventListener('mouseleave', target.__tooltip__.onLeave);
delete target.__tooltip__;
}
}
};
用法
- v-tooltip 取元素自身文本作为提示内容
- v-tooltip="content" 指定提示内容
- v-tooltip="{ content, placement, effect, lines, disabled, maxWidth }"
- v-tooltip.bottom="content" 通过修饰符指定弹出方向
结语
关注我不迷路
不定期发表前端开发技巧类的文章