import type { TooltipStyleProps } from '@antv/component'; import { Tooltip as TooltipComponent } from '@antv/component'; import { get } from '@antv/util'; import type { RuntimeContext } from '../runtime/types'; import type { ElementDatum, ElementType, ID, IElementEvent } from '../types'; import { isToBeDestroyed } from '../utils/element'; import type { BasePluginOptions } from './base-plugin'; import { BasePlugin } from './base-plugin'; /** * 提示框插件配置项 * * Tooltip plugin options */ export interface TooltipOptions extends BasePluginOptions, Pick { /** * 触发行为,可选 hover | click * - `'hover'`:鼠标移入元素时触发 * - `'click'`:鼠标点击元素时触发 * * Trigger behavior, optional hover | click * - `'hover'`:mouse hover element * - `'click'`:mouse click element * @defaultValue 'hover */ trigger?: 'hover' | 'click'; /** * 自定义内容 * * Function for getting tooltip content */ getContent?: (event: IElementEvent, items: ElementDatum[]) => Promise; /** * 是否启用 * * Is enable * @defaultValue true */ enable?: boolean | ((event: IElementEvent, items: ElementDatum[]) => boolean); /** * 显示隐藏的回调 * * Callback executed when visibility of the tooltip card is changed */ onOpenChange: (open: boolean) => void; } /** * 提示框插件 * * Tooltip plugin */ export class Tooltip extends BasePlugin { static defaultOptions: Partial = { trigger: 'hover', position: 'top-right', enterable: false, enable: true, offset: [10, 10], style: { '.tooltip': { visibility: 'hidden', }, }, }; private currentTarget: string | null = null; private tooltipElement: TooltipComponent | null = null; private container: HTMLElement | null = null; constructor(context: RuntimeContext, options: TooltipOptions) { super(context, Object.assign({}, Tooltip.defaultOptions, options)); this.render(); this.bindEvents(); } /** * 获取事件及处理事件的方法 * * Get event and handle event methods * @returns 事件及处理事件的方法 | Event and handling event methods */ private getEvents(): { [key: string]: (event: IElementEvent) => void } { if (this.options.trigger === 'click') { return { 'node:click': this.onClick, 'edge:click': this.onClick, 'combo:click': this.onClick, 'canvas:click': this.onPointerLeave, contextmenu: this.onPointerLeave, drag: this.onPointerLeave, }; } return { 'node:pointerover': this.onPointerOver, 'node:pointermove': this.onPointerMove, 'canvas:pointermove': this.onCanvasMove, 'edge:pointerover': this.onPointerOver, 'edge:pointermove': this.onPointerMove, 'combo:pointerover': this.onPointerOver, 'combo:pointermove': this.onPointerMove, contextmenu: this.onPointerLeave, 'node:drag': this.onPointerLeave, }; } /** * 更新tooltip配置 * * Update the tooltip configuration * @param options - 配置项 | options * @internal */ public update(options: Partial) { this.unbindEvents(); super.update(options); if (this.tooltipElement) { this.container?.removeChild(this.tooltipElement.HTMLTooltipElement); } this.tooltipElement = this.initTooltip(); this.bindEvents(); } private render() { const { canvas } = this.context; const $container = canvas.getContainer(); if (!$container) return; this.container = $container; this.tooltipElement = this.initTooltip(); } private unbindEvents() { const { graph } = this.context; /** The previous event binding needs to be removed when updating the trigger. */ const events = this.getEvents(); Object.keys(events).forEach((eventName) => { graph.off(eventName, events[eventName]); }); } private bindEvents() { const { graph } = this.context; const events = this.getEvents(); Object.keys(events).forEach((eventName) => { graph.on(eventName, events[eventName]); }); } private isEnable = (event: IElementEvent, items: ElementDatum[]) => { const { enable } = this.options; if (typeof enable === 'function') { return enable(event, items); } return enable; }; /** * 点击事件 * * Click event * @param event - 元素 | element */ public onClick = (event: IElementEvent) => { const { target: { id }, } = event; // click the same item twice, tooltip will be hidden if (this.currentTarget === id) { this.hide(event); } else { this.show(event); } }; /** * 在目标元素(node/edge/combo)上移动 * * Move on target element (node/edge/combo) * @param event - 目标元素 | target element */ public onPointerMove = (event: IElementEvent) => { const { target } = event; if (!this.currentTarget || target.id === this.currentTarget) { return; } this.show(event); }; /** * 点击画布/触发拖拽/出现上下文菜单隐藏tooltip * * Hide tooltip when clicking canvas/triggering drag/appearing context menu * @param event - 目标元素 | target element */ public onPointerLeave = (event: IElementEvent) => { this.hide(event); }; /** * 移动画布 * * Move canvas * @param event - 目标元素 | target element */ public onCanvasMove = (event: IElementEvent) => { this.hide(event); }; private onPointerOver = (event: IElementEvent) => { this.show(event); }; /** * 显示目标元素的提示框 * * Show tooltip of target element * @param id - 元素 ID | element ID */ public showById = async (id: ID) => { const event = { target: { id }, } as IElementEvent; await this.show(event); }; private getElementData = (id: ID, targetType: ElementType) => { const { model } = this.context; switch (targetType) { case 'node': return model.getNodeData([id]); case 'edge': return model.getEdgeData([id]); case 'combo': return model.getComboData([id]); default: return []; } }; /** * 在目标元素上显示tooltip * * Show tooltip on target element * @param event - 目标元素 | target element * @internal */ public show = async (event: IElementEvent) => { const { client, target: { id }, } = event; if (isToBeDestroyed(event.target)) return; const targetType = this.context.graph.getElementType(id); const { getContent, title } = this.options; const items: ElementDatum[] = this.getElementData(id, targetType as ElementType); if (!this.tooltipElement || !this.isEnable(event, items)) return; let tooltipContent: { [key: string]: unknown } = {}; if (getContent) { tooltipContent.content = await getContent(event, items); if (!tooltipContent.content) return; } else { const style = this.context.graph.getElementRenderStyle(id); const color = targetType === 'node' ? style.fill : style.stroke; tooltipContent = { title: title || targetType, data: items.map((item) => { return { name: 'ID', value: item.id || `${item.source} -> ${item.target}`, color, }; }), }; } this.currentTarget = id; let x; let y; if (client) { x = client.x; y = client.y; } else { const style = get(items, '0.style', { x: 0, y: 0 }); x = style.x; y = style.y; } this.options.onOpenChange?.(true); this.tooltipElement.update({ ...this.tooltipStyleProps, x, y, style: { '.tooltip': { visibility: 'visible', }, }, ...tooltipContent, }); }; /** * 隐藏tooltip * * Hidden tooltip * @param event - 目标元素,不传则为外部调用 | Target element, not passed in as external call */ public hide = (event?: IElementEvent) => { // if e is undefined, hide the tooltip, external call if (!event) { this.options.onOpenChange?.(false); this.tooltipElement?.hide(); this.currentTarget = null; return; } if (!this.tooltipElement) return; // No target node: tooltip has been hidden. No need for duplicated call. if (!this.currentTarget) return; const { client: { x, y }, } = event; this.options.onOpenChange?.(false); this.tooltipElement.hide(x, y); this.currentTarget = null; }; private get tooltipStyleProps() { const { canvas } = this.context; const { center } = canvas.getBounds(); const $container = canvas.getContainer() as HTMLElement; const { top, left } = $container.getBoundingClientRect(); const { style, position, enterable, container = { x: -left, y: -top }, title, offset } = this.options; const [x, y] = center; const [width, height] = canvas.getSize(); return { x, y, container, title, bounding: { x: 0, y: 0, width, height }, position, enterable, offset, style, }; } private initTooltip = () => { const tooltipElement = new TooltipComponent({ className: 'tooltip', style: this.tooltipStyleProps, }); this.container?.appendChild(tooltipElement.HTMLTooltipElement); return tooltipElement; }; /** * 销毁tooltip * * Destroy tooltip * @internal */ public destroy(): void { this.unbindEvents(); if (this.tooltipElement) { this.container?.removeChild(this.tooltipElement.HTMLTooltipElement); } super.destroy(); } }