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();
}
}