问题背景
在项目中,由于支持了国际化,导致语言为英文时,部分文字会超出元素。故有些地方需要使用“...”方式来截断文字并显示气泡,当没有截断文字时不显示气泡,ng-zorro原生tooltip无法支持,需要继承该指令并自定义逻辑。
实现方案
继承NzTooltipDirective,完成初始化参数传递
export class VisibleTooltipDirective extends NzTooltipDirective implements AfterViewInit {
@Input() nzDynamicVisibleTooltip: boolean = true;
readonly SPECIAL_CHARS_REGEXP: RegExp = /([\:\-\_]+(.))/g;
readonly MOZ_HACK_REGEXP: RegExp = /^moz([A-Z])/;
private overlayElement: HTMLElement;
private timer: NodeJS.Timeout = null;
/**
* 初始化构造函数
*
* @param el 模版
* @param render
*/
constructor(
elementRef: ElementRef,
hostView: ViewContainerRef,
resolver: ComponentFactoryResolver,
renderer: Renderer2,
@Host() @Optional() noAnimation?: NzNoAnimationDirective
) {
super(elementRef, hostView, resolver, renderer, noAnimation);
}
}
注册自定义鼠标监听方法,通过判断元素clientWidth与scrollWidth的关系来控制气泡的显隐。由于scrollWidth和clientWidth都会四舍五入取整数,所以存在文字溢出scrollWidth===clientWidth,故这里利用Range来计算scrollWidth的精确宽度。
/**
* 注册监听鼠标移入移出方法
*/
registerTriggersOfNull(): void {
this.triggerDisposables.push(
this.renderer.listen(this.elementRef.nativeElement, 'mouseenter', () => {
if (!this.nzDynamicVisibleTooltip) return;
clearTimeout(this.timer);
const range: Range = document.createRange();
range.setStart(this.elementRef.nativeElement, 0);
range.setEnd(this.elementRef.nativeElement, this.elementRef.nativeElement.childNodes.length);
// 计算滚动内容精确宽度,保留两位小数
const rangeWidth: number = Math.floor(range.getBoundingClientRect().width * 100) / 100;
// 计算内边距
const padding: number = (parseInt(this.getStyle(this.elementRef.nativeElement, 'paddingLeft'), 10) || 0) +
(parseInt(this.getStyle(this.elementRef.nativeElement, 'paddingRight'), 10) || 0);
// 容器精确宽度,保留两位小数
const containerWidth: number = Math.floor(this.elementRef.nativeElement.getBoundingClientRect().width * 100) / 100;
// 判断text-overflow是否溢出,溢出则显示气泡
if ((rangeWidth + padding > containerWidth || this.elementRef.nativeElement.scrollWidth > this.elementRef.nativeElement.offsetWidth)) {
this.delayEnterLeaveOverride(true, true, this._mouseEnterDelay);
} else {
this.hide();
}
})
);
this.triggerDisposables.push(
this.renderer.listen(this.elementRef.nativeElement, 'mouseleave', () => {
if (!this.nzDynamicVisibleTooltip) return;
this.delayEnterLeaveOverride(true, false, this._mouseLeaveDelay);
if (this.component.overlay.overlayRef?.overlayElement && !this.overlayElement) {
this.overlayElement = this.component.overlay.overlayRef?.overlayElement;
this.triggerDisposables.push(
this.renderer.listen(this.overlayElement, 'mouseenter', () => {
this.delayEnterLeaveOverride(false, true, this._mouseEnterDelay);
})
);
this.triggerDisposables.push(
this.renderer.listen(this.overlayElement, 'mouseleave', () => {
this.delayEnterLeaveOverride(false, false, this._mouseEnterDelay);
})
);
}
})
);
}
在ngAfterViewInit初始化气泡配置
/**
* dom渲染完成,可获取dom节点
*/
ngAfterViewInit(): void {
// 配合截断文字动态显示气泡
if (this.nzDynamicVisibleTooltip) {
this.trigger = null;
}
this.createComponent();
if (this.nzDynamicVisibleTooltip) {
this.registerTriggersOfNull();
} else {
this.registerTriggers();
}
}
气泡显隐执行方法
/**
* 延迟执行
*
* @param isOrigin
* @param isEnter
* @param delay
*/
private delayEnterLeaveOverride(isOrigin: boolean, isEnter: boolean, delay: number = -1): void {
if (this.timer) {
this.clearTogglingTimerOverride();
} else if (delay > 0) {
this.timer = setTimeout(() => {
this.timer = undefined;
isEnter ? this.show() : this.hide();
}, delay * 1000);
} else {
isEnter && isOrigin ? this.show() : this.hide();
}
}
/**
* 清除延时器
*/
private clearTogglingTimerOverride(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
}
💡 Tips:由于目前项目只是需要根据截断文字显隐气泡,故通过调用show和hide方法就能够实现。如有更复杂的需求,还可重写@Input属性和其余方法。也可以通过重写componentFactory自定义气泡组件。具体可以看看Tooltip源码(NzTooltipDirective、NzToolTipComponent、NzTooltipBaseDirective、NzTooltipBaseComponent).
完整代码
import {
Directive,
ElementRef,
Renderer2,
ViewContainerRef,
ComponentFactoryResolver,
Host,
Optional,
AfterViewInit,
Input
} from '@angular/core';
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
import { NzTooltipDirective } from 'ng-zorro-antd/tooltip';
@Directive({
selector: '[appVisibleTooltip]'
})
export class VisibleTooltipDirective extends NzTooltipDirective implements AfterViewInit {
@Input() nzDynamicVisibleTooltip: boolean = true;
readonly SPECIAL_CHARS_REGEXP: RegExp = /([\:\-\_]+(.))/g;
readonly MOZ_HACK_REGEXP: RegExp = /^moz([A-Z])/;
private overlayElement: HTMLElement;
private timer: NodeJS.Timeout = null;
/**
* 初始化构造函数
*
* @param el 模版
* @param render
*/
constructor(
elementRef: ElementRef,
hostView: ViewContainerRef,
resolver: ComponentFactoryResolver,
renderer: Renderer2,
@Host() @Optional() noAnimation?: NzNoAnimationDirective
) {
super(elementRef, hostView, resolver, renderer, noAnimation);
}
/**
* dom渲染完成,可获取dom节点
*/
ngAfterViewInit(): void {
// 配合截断文字动态显示气泡
if (this.nzDynamicVisibleTooltip) {
this.trigger = null;
}
this.createComponent();
if (this.nzDynamicVisibleTooltip) {
this.registerTriggersOfNull();
} else {
this.registerTriggers();
}
}
/**
* 注册监听鼠标移入移出方法
*/
registerTriggersOfNull(): void {
this.triggerDisposables.push(
this.renderer.listen(this.elementRef.nativeElement, 'mouseenter', () => {
if (!this.nzDynamicVisibleTooltip) return;
clearTimeout(this.timer);
const range: Range = document.createRange();
range.setStart(this.elementRef.nativeElement, 0);
range.setEnd(this.elementRef.nativeElement, this.elementRef.nativeElement.childNodes.length);
// 计算滚动内容精确宽度,保留两位小数
const rangeWidth: number = Math.floor(range.getBoundingClientRect().width * 100) / 100;
// 计算内边距
const padding: number = (parseInt(this.getStyle(this.elementRef.nativeElement, 'paddingLeft'), 10) || 0) +
(parseInt(this.getStyle(this.elementRef.nativeElement, 'paddingRight'), 10) || 0);
// 容器精确宽度,保留两位小数
const containerWidth: number = Math.floor(this.elementRef.nativeElement.getBoundingClientRect().width * 100) / 100;
// 判断text-overflow是否溢出,溢出则显示气泡
if ((rangeWidth + padding > containerWidth || this.elementRef.nativeElement.scrollWidth > this.elementRef.nativeElement.offsetWidth)) {
this.delayEnterLeaveOverride(true, true, this._mouseEnterDelay);
} else {
this.hide();
}
})
);
this.triggerDisposables.push(
this.renderer.listen(this.elementRef.nativeElement, 'mouseleave', () => {
if (!this.nzDynamicVisibleTooltip) return;
this.delayEnterLeaveOverride(true, false, this._mouseLeaveDelay);
if (this.component.overlay.overlayRef?.overlayElement && !this.overlayElement) {
this.overlayElement = this.component.overlay.overlayRef?.overlayElement;
this.triggerDisposables.push(
this.renderer.listen(this.overlayElement, 'mouseenter', () => {
this.delayEnterLeaveOverride(false, true, this._mouseEnterDelay);
})
);
this.triggerDisposables.push(
this.renderer.listen(this.overlayElement, 'mouseleave', () => {
this.delayEnterLeaveOverride(false, false, this._mouseEnterDelay);
})
);
}
})
);
}
/**
* 延迟执行
*
* @param isOrigin
* @param isEnter
* @param delay
*/
private delayEnterLeaveOverride(isOrigin: boolean, isEnter: boolean, delay: number = -1): void {
if (this.timer) {
this.clearTogglingTimerOverride();
} else if (delay > 0) {
this.timer = setTimeout(() => {
this.timer = undefined;
isEnter ? this.show() : this.hide();
}, delay * 1000);
} else {
isEnter && isOrigin ? this.show() : this.hide();
}
}
/**
* 清除延时器
*/
private clearTogglingTimerOverride(): void {
if (this.timer) {
clearTimeout(this.timer);
this.timer = undefined;
}
}
/**
*
* @param element
* @param styleName
* @returns
*/
private getStyle(element: any, styleName: string): string {
if (!element || !styleName) return null;
styleName = this.camelCase(styleName);
if (styleName === 'float') {
styleName = 'cssFloat';
}
try {
var computed: any = document.defaultView.getComputedStyle(element, '');
return element.style[styleName] || computed ? computed[styleName] : null;
} catch (e) {
return element.style[styleName];
}
}
/**
*
* @param name
* @returns
*/
camelCase(name: string): string {
return name.replace(this.SPECIAL_CHARS_REGEXP, (_, separator, letter, offset) => {
return offset ? letter.toUpperCase() : letter;
}).replace(this.MOZ_HACK_REGEXP, 'Moz$1');
};
}
💡 Tips:欢迎小伙伴们共同讨论更优方法哦!!!