背景
Overlay
OverlayRef的attach()支持ComponentPortal和TemplatePortal等,為了統一管理overlay的內容,我們需要創建一個OverlayToolTipComponent用來展示具體的tooltip
@Component({selector: 'overlay-tooltip-inner',template: `<div class="overlay-tooltip-inner">@if (text) {<div>{{ text }}</div>} @else {<ng-container *ngTemplateOutlet="contentTemplate"></ng-container>}</div>`,styles: [`.overlay-tooltip-inner {padding: 5px;background-color:rgb(207, 229, 248);border-radius: 4px;box-shadow: 0px 2px 6px rgba(0, 0, 0, 0.2);}`],standalone: false }) export class OverlayToolTipComponent {@Input()set overlayTooltip(tooltip: string | TemplateRef<any>) {if (_.isString(tooltip)) {this.text = tooltip;} else {this.contentTemplate = tooltip;}}text: string;contentTemplate: TemplateRef<any>;constructor() {//} }
OverlayToolTipDirective
接下來創建OverlayToolTipDirective,它接受的tooltip參數類型是string | TemplateRef<any>
@Directive({selector: '[overlayTooltip]',standalone: false }) export class OverlayToolTipDirective implements OnChanges, OnDestroy {private _overlayRef: OverlayRef = undefined;private _tooltip: string | TemplateRef<any> = '';@Input()set overlayTooltip(tooltip: string | TemplateRef<any>) {this._tooltip = tooltip ?? '';}private flexibleConnectedPositionStrategy: FlexibleConnectedPositionStrategy;constructor(private _overlay: Overlay,private _overlayPositionBuilder: OverlayPositionBuilder,private _elementRef: ElementRef) {//}ngOnChanges(changes: SimpleChanges): void {if (_.size(this._tooltip) > 0) {this.updateFlexibleConnectedPositionStrategy();this.bindingTriggers();}}private updateFlexibleConnectedPositionStrategy() {this.flexibleConnectedPositionStrategy = this._overlayPositionBuilder.flexibleConnectedTo(this._elementRef).withPositions([this.createPosition('center', 'top', 'center', 'bottom')]);}private generateOverlayRef() {if (!this.flexibleConnectedPositionStrategy) {this.updateFlexibleConnectedPositionStrategy();}this._overlayRef = this._overlay.create({ positionStrategy: this.flexibleConnectedPositionStrategy });}private createPosition(originX: HorizontalConnectionPos, originY: VerticalConnectionPos,overlayX: HorizontalConnectionPos, overlayY: VerticalConnectionPos): ConnectionPositionPair {return { originX, originY, overlayX, overlayY };}private bindingTriggers() {this._elementRef.nativeElement.addEventListener('mouseover', this.show());this._elementRef.nativeElement.addEventListener('mouseout', this.hide());}private show() {if (!this._overlayRef) {this.generateOverlayRef();}if (this._overlayRef && !this._overlayRef.hasAttached()) {const tooltipRef: ComponentRef<OverlayToolTipComponent> = this._overlayRef.attach(new ComponentPortal(OverlayToolTipComponent));tooltipRef.instance.overlayTooltip = this._tooltip;}}private hide() {if (!_.isEmpty(this._overlayRef) && this._overlayRef.hasAttached()) {this._overlayRef.detach();}}private cleanUpOverlayRef() {if (this._overlayRef?.dispose) {this._overlayRef.dispose();this._overlayRef = undefined;}}ngOnDestroy() {this.cleanUpOverlayRef();this.removeExistingListeners();}removeExistingListeners() {this._elementRef.nativeElement.removeEventListener('mouseover', this.show());this._elementRef.nativeElement.removeEventListener('mouseout', this.hide());} }
效果如下:
位置自適應
由上圖可以看出,當位置不夠容納tooltip時,目標元素會被遮擋。所以我們需要添加placement和autoPosition允許用戶指定tooltip的位置和tooltip是否可以自適應位置
通過OverlayPositionBuilder的withPositions()設置position數組。
class ConnectionPositionPairExt extends ConnectionPositionPair {sort: number; }export class OverlayToolTipDirective implements OnChanges, OnDestroy { ...@Input() placement: 'top' | 'bottom' | 'left' | 'right' = 'top';@Input() autoPosition = true;// updateFlexibleConnectedPositionStrategy() 更改如下:private updateFlexibleConnectedPositionStrategy() {this.flexibleConnectedPositionStrategy = this._overlayPositionBuilder.flexibleConnectedTo(this._elementRef).withPositions(this.getAvailablePositions());}private getAvailablePositions(): ConnectionPositionPairExt[] {// 生成四個方向的默認位置配置const positions = [this.createPosition('center', 'top', 'center', 'bottom', 1), // topthis.createPosition('start', 'center', 'end', 'center', 2), // leftthis.createPosition('center', 'bottom', 'center', 'top', 3), // bottomthis.createPosition('end', 'center', 'start', 'center', 4), // right];// 根據當前 placement 設置優先級const priorityMap: { [key in string]: number } = {['bottom']: 2,['left']: 1,['right']: 3,};positions[priorityMap[this.placement] || 0].sort = 0;// 返回排序后的位置配置return this.autoPosition ? positions.sort((a, b) => a.sort - b.sort) : [positions[priorityMap[this.placement] || 0]];} ... }
效果如下,string或者template
總結
這樣我們就在不引入其他庫的前提下完成了一個內容豐富位置靈活的tooltip組件啦。
要注意,在tooltip被觸發時再創建OverlayRef以避免不必要的性能開銷。當tooltip隱藏和Directive銷毀時,刪除事件監聽并調用OverlayRef的detach()和dispose()。
另外,Overlay的ConnectedPosition還可以指定tooltip和目標元素之間的距離,也可以增加panelClass以便深度定制tooltip的內容。