@antv/g6
Version:
A Graph Visualization Framework in JavaScript
220 lines (196 loc) • 7 kB
text/typescript
import { isFunction } from '@antv/util';
import { CommonEvent } from '../constants';
import { ELEMENT_TYPES } from '../constants/element';
import type { RuntimeContext } from '../runtime/types';
import type { EdgeDirection, Element, ElementType, ID, IDragEvent, IPointerEvent, State } from '../types';
import { isToBeDestroyed } from '../utils/element';
import { idsOf } from '../utils/id';
import { getElementNthDegreeIds } from '../utils/relation';
import type { BaseBehaviorOptions } from './base-behavior';
import { BaseBehavior } from './base-behavior';
/**
* <zh/> 悬浮元素交互配置项
*
* <en/> Hover element behavior options
*/
export interface HoverActivateOptions extends BaseBehaviorOptions {
/**
* <zh/> 是否启用动画
*
* <en/> Whether to enable animation
* @defaultValue true
*/
animation?: boolean;
/**
* <zh/> 是否启用悬浮元素的功能
*
* <en/> Whether to enable hover element function
* @defaultValue true
*/
enable?: boolean | ((event: IPointerEvent) => boolean);
/**
* <zh/> 激活元素的n度关系
* - 默认为 `0`,表示只激活当前节点
* - `1` 表示激活当前节点及其直接相邻的节点和边,以此类推
*
* <en/> N-degree relationship of the hovered element
* - default to `0`, which means only the current node is activated
* - `1` means the current node and its directly adjacent nodes and edges are activated, etc
* @defaultValue 0
*/
degree?: number | ((event: IPointerEvent) => number);
/**
* <zh/> 指定边的方向
* - `'both'`: 表示激活当前节点的所有关系
* - `'in'`: 表示激活当前节点的入边和入节点
* - `'out'`: 表示激活当前节点的出边和出节点
*
* <en/> Specify the direction of the edge
* - `'both'`: Activate all relationships of the current node
* - `'in'`: Activate the incoming edges and nodes of the current node
* - `'out'`: Activate the outgoing edges and nodes of the current node
* @defaultValue 'both'
*/
direction?: EdgeDirection;
/**
* <zh/> 激活元素的状态,默认为 `active`
*
* <en/> Active element state, default to`active`
* @defaultValue 'active'
*/
state?: State;
/**
* <zh/> 非激活元素的状态,默认为不改变
*
* <en/> Inactive element state, default to no change
*/
inactiveState?: State;
/**
* <zh/> 当元素被悬停时的回调
*
* <en/> Callback when the element is hovered
*/
onHover?: (event: IPointerEvent) => void;
/**
* <zh/> 当悬停结束时的回调
*
* <en/> Callback when the hover ends
*/
onHoverEnd?: (event: IPointerEvent) => void;
}
/**
* <zh/> 悬浮元素交互
*
* <en/> Hover element behavior
* @remarks
* <zh/> 当鼠标悬停在元素上时,可以激活元素的状态,例如高亮节点或边。
*
* <en/> When the mouse hovers over an element, you can activate the state of the element, such as highlighting nodes or edges.
*/
export class HoverActivate extends BaseBehavior<HoverActivateOptions> {
static defaultOptions: Partial<HoverActivateOptions> = {
animation: false,
enable: true,
degree: 0,
direction: 'both',
state: 'active',
inactiveState: undefined,
};
private isFrozen = false;
constructor(context: RuntimeContext, options: HoverActivateOptions) {
super(context, Object.assign({}, HoverActivate.defaultOptions, options));
this.bindEvents();
}
private toggleFrozen = (e: IDragEvent) => {
this.isFrozen = e.type === 'dragstart';
};
private bindEvents() {
const { graph } = this.context;
this.unbindEvents();
ELEMENT_TYPES.forEach((type) => {
graph.on(`${type}:${CommonEvent.POINTER_ENTER}`, this.hoverElement);
graph.on(`${type}:${CommonEvent.POINTER_LEAVE}`, this.hoverElement);
});
const canvas = this.context.canvas.document;
canvas.addEventListener(`${CommonEvent.DRAG_START}`, this.toggleFrozen);
canvas.addEventListener(`${CommonEvent.DRAG_END}`, this.toggleFrozen);
}
private hoverElement = (event: IPointerEvent<Element>) => {
if (!this.validate(event)) return;
const isEnter = event.type === CommonEvent.POINTER_ENTER;
this.updateElementsState(event, isEnter);
const { onHover, onHoverEnd } = this.options;
if (isEnter) onHover?.(event);
else onHoverEnd?.(event);
};
protected getActiveIds(event: IPointerEvent<Element>) {
const { graph } = this.context;
const { degree, direction } = this.options;
const elementId = event.target.id;
return degree
? getElementNthDegreeIds(
graph,
event.targetType as ElementType,
elementId,
typeof degree === 'function' ? degree(event) : degree,
direction,
)
: [elementId];
}
private updateElementsState = (event: IPointerEvent<Element>, add: boolean) => {
if (!this.options.state && !this.options.inactiveState) return;
const { graph } = this.context;
const { state, animation, inactiveState } = this.options;
const activeIds = this.getActiveIds(event);
const states: Record<ID, State[]> = {};
if (state) {
Object.assign(states, this.getElementsState(activeIds, state, add));
}
if (inactiveState) {
const inactiveIds = idsOf(graph.getData(), true).filter((id) => !activeIds.includes(id));
Object.assign(states, this.getElementsState(inactiveIds, inactiveState, add));
}
graph.setElementState(states, animation);
};
private getElementsState = (ids: ID[], state: State, add: boolean) => {
const { graph } = this.context;
const states: Record<ID, State[]> = {};
ids.forEach((id) => {
const currentState = graph.getElementState(id);
if (add) {
states[id] = currentState.includes(state) ? currentState : [...currentState, state];
} else {
states[id] = currentState.filter((s) => s !== state);
}
});
return states;
};
private validate(event: IPointerEvent<Element>) {
if (
this.destroyed ||
this.isFrozen ||
isToBeDestroyed(event.target) ||
// @ts-expect-error private property
// 避免动画冲突,在combo/node折叠展开过程中不触发
this.context.graph.isCollapsingExpanding
)
return false;
const { enable } = this.options;
if (isFunction(enable)) return enable(event);
return !!enable;
}
private unbindEvents() {
const { graph } = this.context;
ELEMENT_TYPES.forEach((type) => {
graph.off(`${type}:${CommonEvent.POINTER_ENTER}`, this.hoverElement);
graph.off(`${type}:${CommonEvent.POINTER_LEAVE}`, this.hoverElement);
});
const canvas = this.context.canvas.document;
canvas.removeEventListener(`${CommonEvent.DRAG_START}`, this.toggleFrozen);
canvas.removeEventListener(`${CommonEvent.DRAG_END}`, this.toggleFrozen);
}
public destroy() {
this.unbindEvents();
super.destroy();
}
}