refactor: define and export event (#5627)

* feat(utils): add isElement util

* refactor(runtime): override on/once api to provide type

* refactor: define and export event type

* refactor(runtime): carry originalTarget in element event
This commit is contained in:
Aaron 2024-04-07 18:54:15 +08:00 committed by GitHub
parent 944ccda096
commit a428667dae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 214 additions and 69 deletions

View File

@ -20,15 +20,22 @@ describe('behavior combo expand collapse', () => {
it('collapse', async () => {
// collapse combo-2
graph.emit(`combo:${CommonEvent.DBLCLICK}`, { target: { id: 'combo-2' } });
// @ts-expect-error private method
const combo2 = graph.context.element?.getElement('combo-2');
graph.emit(`combo:${CommonEvent.DBLCLICK}`, { target: combo2 });
await expect(graph).toMatchSnapshot(__filename, 'collapse-combo-2');
});
it('expand', async () => {
// expand combo-2
graph.emit(`combo:${CommonEvent.DBLCLICK}`, { target: { id: 'combo-2' } });
// @ts-expect-error private method
const combo2 = graph.context.element?.getElement('combo-2');
graph.emit(`combo:${CommonEvent.DBLCLICK}`, { target: combo2 });
// expand combo-1
graph.emit(`combo:${CommonEvent.DBLCLICK}`, { target: { id: 'combo-1' } });
// @ts-expect-error private method
const combo1 = graph.context.element?.getElement('combo-1');
graph.emit(`combo:${CommonEvent.DBLCLICK}`, { target: combo1 });
await expect(graph).toMatchSnapshot(__filename, 'expand-combo-1');
});
});

View File

@ -0,0 +1,32 @@
import { IPointerEvent, NodeEvent } from '@/src';
import { createGraph } from '@@/utils';
import type { DisplayObject } from '@antv/g';
import { CustomEvent } from '@antv/g';
describe('element event', () => {
it('element event target', async () => {
const graph = createGraph({
data: {
nodes: [{ id: 'node-1' }, { id: 'node-2' }],
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2' }],
},
});
const click = jest.fn();
graph.on<IPointerEvent>(`node:${NodeEvent.CLICK}`, click);
await graph.draw();
// @ts-expect-error context is private
const node1 = graph.context.element!.getElement('node-1')!;
(node1.children[0] as DisplayObject).dispatchEvent(new CustomEvent('click', {}));
expect(click).toHaveBeenCalledTimes(1);
expect(click.mock.calls[0][0].target).toBe(node1);
expect(click.mock.calls[0][0].targetType).toBe('node');
expect(click.mock.calls[0][0].originalTarget).toBe(node1.children[0]);
graph.destroy();
});
});

View File

@ -14,6 +14,7 @@ import {
getTrianglePoints,
getTrianglePorts,
isEdge,
isElement,
isNode,
isSameNode,
isSimplePort,
@ -38,14 +39,20 @@ describe('element', () => {
const edge = new Polyline({ style: { sourceNode: node1, targetNode: node2 } });
it('isNode', () => {
expect(isNode(new Rect({ style: { width: 10, height: 10 } }))).toBe(false);
const rect = new Rect({ style: { width: 10, height: 10 } });
expect(isNode(rect)).toBe(false);
expect(isElement(rect)).toBe(false);
const node = new Circle({});
expect(isNode(node)).toBe(true);
expect(isElement(node)).toBe(true);
});
it('isEdge', () => {
expect(isEdge(new Line({ style: { x1: 0, y1: 0, x2: 10, y2: 10 } }))).toBe(false);
const line = new Line({ style: { x1: 0, y1: 0, x2: 10, y2: 10 } });
expect(isEdge(line)).toBe(false);
expect(isElement(line)).toBe(false);
expect(isEdge(edge)).toBe(true);
expect(isElement(edge)).toBe(true);
});
it('isSameNode', () => {

View File

@ -1,5 +1,4 @@
import type { Graph } from '@/src';
import type { AnimateEvent } from '@/src/utils/event';
import type { Graph, IAnimateEvent } from '@/src';
import type { Canvas, IAnimation } from '@antv/g';
import * as fs from 'fs';
import * as path from 'path';
@ -108,7 +107,7 @@ export async function toMatchAnimation(
options: ToMatchSVGSnapshotOptions = {},
) {
const animationPromise = new Promise<IAnimation>((resolve) => {
graph.once('beforeanimate', (e: AnimateEvent) => {
graph.once<IAnimateEvent>('beforeanimate', (e) => {
resolve(e.animation!);
});
});

View File

@ -3,6 +3,7 @@ import { isFunction } from '@antv/util';
import { CommonEvent } from '../constants';
import type { RuntimeContext } from '../runtime/types';
import type { IPointerEvent } from '../types';
import { isElement } from '../utils/element';
import type { BaseBehaviorOptions } from './base-behavior';
import { BaseBehavior } from './base-behavior';
@ -59,8 +60,10 @@ export class CollapseExpand extends BaseBehavior<CollapseExpandOptions> {
private onCollapseExpand = async (event: IPointerEvent) => {
if (!this.validate(event)) return;
const { target } = event;
if (!isElement(target)) return;
const id = event?.target?.id;
const id = target.id;
const { model, graph } = this.context;
const data = model.getComboData([id])[0];
if (!data) return false;

View File

@ -4,7 +4,7 @@ import { CommonEvent } from '../constants';
import type { RuntimeContext } from '../runtime/types';
import type { EdgeData } from '../spec';
import type { EdgeStyle } from '../spec/element/edge';
import type { IPointerEvent } from '../types';
import type { Element, IPointerEvent } from '../types';
import type { BaseBehaviorOptions } from './base-behavior';
import { BaseBehavior } from './base-behavior';
@ -85,7 +85,7 @@ export class CreateEdge extends BaseBehavior<CreateEdgeOptions> {
graph.on(CommonEvent.POINTER_MOVE, this.updateAssistEdge);
}
private drop = async (event: IPointerEvent) => {
private drop = async (event: IElementEvent) => {
const { targetType } = event;
if (['combo', 'node'].includes(targetType) && this.source) {
await this.handleCreateEdge(event);
@ -94,7 +94,7 @@ export class CreateEdge extends BaseBehavior<CreateEdgeOptions> {
}
};
private handleCreateEdge = async (event: IPointerEvent) => {
private handleCreateEdge = async (event: IElementEvent) => {
if (!this.validate(event)) return;
const { graph, canvas } = this.context;
const { style } = this.options;
@ -140,7 +140,7 @@ export class CreateEdge extends BaseBehavior<CreateEdgeOptions> {
await element!.draw({ animation: false, silence: true });
};
private createEdge = (event: IPointerEvent) => {
private createEdge = (event: IElementEvent) => {
const { graph } = this.context;
const { style, onFinish, onCreate } = this.options;
const targetId = event.target?.id;
@ -202,3 +202,5 @@ export class CreateEdge extends BaseBehavior<CreateEdgeOptions> {
super.destroy();
}
}
type IElementEvent = IPointerEvent<Element>;

View File

@ -4,7 +4,7 @@ import { ID } from '@antv/graphlib';
import { isFunction } from '@antv/util';
import { COMBO_KEY, CommonEvent } from '../constants';
import { RuntimeContext } from '../runtime/types';
import type { EdgeDirection, IDragEvent, Point, PrefixObject } from '../types';
import type { EdgeDirection, Element, IDragEvent, Point, PrefixObject } from '../types';
import { getBBoxSize, getCombinedBBox } from '../utils/bbox';
import { idOf } from '../utils/id';
import { subStyleProps } from '../utils/prefix';
@ -24,7 +24,7 @@ export interface DragElementOptions extends BaseBehaviorOptions, PrefixObject<Ba
*
* <en/> Whether to enable the function of dragging the node
*/
enable?: boolean | ((event: IDragEvent) => boolean);
enable?: boolean | ((event: IElementDragEvent) => boolean);
/**
* <zh/>
* - link: 将拖拽元素置入为目标元素的子元素
@ -150,7 +150,7 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
);
}
private onDragStart = (event: IDragEvent) => {
private onDragStart = (event: IElementDragEvent) => {
this.enable = this.validate(event);
if (!this.enable) return;
@ -160,7 +160,7 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
if (this.options.shadow) this.createShadow(this.target);
};
private onDrag = (event: IDragEvent) => {
private onDrag = (event: IElementDragEvent) => {
if (!this.enable) return;
const zoom = this.context.graph.getZoom();
const { dx, dy } = event;
@ -184,7 +184,7 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
this.target = [];
};
private onDrop = async (event: IDragEvent) => {
private onDrop = async (event: IElementDragEvent) => {
if (this.options.dropEffect !== 'link') return;
const { model, element } = this.context;
const modifiedParentId = event.target.id;
@ -200,7 +200,7 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
await element?.draw({ animation: true });
};
private validate(event: IDragEvent) {
private validate(event: IElementDragEvent) {
if (this.destroyed) return false;
const { enable } = this.options;
if (isFunction(enable)) return enable(event);
@ -294,3 +294,5 @@ export class DragElement extends BaseBehavior<DragElementOptions> {
super.destroy();
}
}
type IElementDragEvent = IDragEvent<Element>;

View File

@ -2,7 +2,7 @@ import type { ID } from '@antv/graphlib';
import { isFunction } from '@antv/util';
import { CommonEvent } from '../constants';
import type { RuntimeContext } from '../runtime/types';
import type { IKeyboardEvent, IPointerEvent, ViewportAnimationEffectTiming } from '../types';
import type { Element, IPointerEvent, ViewportAnimationEffectTiming } from '../types';
import type { BaseBehaviorOptions } from './base-behavior';
import { BaseBehavior } from './base-behavior';
@ -20,7 +20,7 @@ export interface FocusElementOptions extends BaseBehaviorOptions {
*
* <en/> Whether to enable the function of dragging the node
*/
enable?: boolean | ((event: IPointerEvent | IKeyboardEvent) => boolean);
enable?: boolean | ((event: IElementEvent) => boolean);
}
export class FocusElement extends BaseBehavior<FocusElementOptions> {
@ -57,16 +57,16 @@ export class FocusElement extends BaseBehavior<FocusElementOptions> {
);
}
private clickFocusElement = async (event: IPointerEvent) => {
private clickFocusElement = async (event: IElementEvent) => {
if (!this.validate(event)) return;
const { animation } = this.options;
const { graph } = this.context;
const id = this.getSelectedNodeIDs([event.target.id]);
const id = this.getSelectedNodeIDs([(event.target as Element).id]);
await graph.focusElement(id, animation);
};
private validate(event: IPointerEvent) {
private validate(event: IElementEvent) {
if (this.destroyed) return false;
const { enable } = this.options;
if (isFunction(enable)) return enable(event);
@ -86,3 +86,5 @@ export class FocusElement extends BaseBehavior<FocusElementOptions> {
super.destroy();
}
}
type IElementEvent = IPointerEvent<Element>;

View File

@ -3,7 +3,7 @@ import { isFunction } from '@antv/util';
import { CommonEvent } from '../constants';
import { ELEMENT_TYPES } from '../constants/element';
import type { RuntimeContext } from '../runtime/types';
import type { ElementType, IPointerEvent, State } from '../types';
import type { Element, ElementType, IPointerEvent, State } from '../types';
import { getIds } from '../utils/id';
import { getElementNthDegreeIds } from '../utils/relation';
import type { BaseBehaviorOptions } from './base-behavior';
@ -96,7 +96,12 @@ export class HoverElement extends BaseBehavior<HoverElementOptions> {
const { graph } = this.context;
const { targetType, target } = event;
const activeIds = getElementNthDegreeIds(graph, targetType as ElementType, target.id, this.options.degree);
const activeIds = getElementNthDegreeIds(
graph,
targetType as ElementType,
(target as Element).id,
this.options.degree,
);
const states: Record<ID, State[]> = {};

View File

@ -65,10 +65,14 @@ export type {
ViewportOptions,
} from './spec';
export type {
IAnimateEvent,
IDragEvent,
IElementEvent,
IElementLifeCycleEvent,
IEvent,
IGraphLifeCycleEvent,
IKeyboardEvent,
IPointerEvent,
IViewportEvent,
IWheelEvent,
Point,
Vector2,

View File

@ -1,5 +1,6 @@
import type { RuntimeContext } from '../runtime/types';
import type { IElementEvent } from '../types/event';
import type { Element } from '../types';
import type { IPointerEvent } from '../types/event';
import type { Item } from '../utils/contextmenu';
import { CONTEXTMENU_CSS, getContentFromItems } from '../utils/contextmenu';
import { createPluginContainer, insertDOM } from '../utils/dom';
@ -196,3 +197,5 @@ export class Contextmenu extends BasePlugin<ContextmenuOptions> {
}
};
}
type IElementEvent = IPointerEvent<Element>;

View File

@ -2,7 +2,7 @@ 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, IElementEvent } from '../types';
import type { Element, ElementDatum, ElementType, IPointerEvent } from '../types';
import type { BasePluginOptions } from './base-plugin';
import { BasePlugin } from './base-plugin';
@ -39,14 +39,13 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
this.bindEvents();
}
public getEvents(): { [key: string]: Function } {
public 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,
afterremoveitem: this.onPointerLeave,
contextmenu: this.onPointerLeave,
drag: this.onPointerLeave,
};
@ -176,7 +175,7 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
const { getContent } = this.options;
const { color, stroke } = attributes;
this.currentTarget = id;
const items: ElementDatum[] = this.getItems(id, targetType);
const items: ElementDatum[] = this.getItems(id, targetType as ElementType);
let x;
let y;
if (client) {
@ -266,3 +265,5 @@ export class Tooltip extends BasePlugin<TooltipOptions> {
super.destroy();
}
}
type IElementEvent = IPointerEvent<Element>;

View File

@ -1,7 +1,7 @@
import type EventEmitter from '@antv/event-emitter';
import { getExtension } from '..';
import type { RuntimeContext } from '../../runtime/types';
import type { Listener } from '../../types';
import type { IEvent } from '../../types';
import { arrayDiff } from '../../utils/diff';
import { parseExtensions } from '../../utils/extension';
import type { ExtensionOptions, LooselyExtensionOption, STDExtensionOption } from './types';
@ -88,7 +88,7 @@ export class BaseExtension<T extends LooselyExtensionOption> {
protected options: Required<T>;
protected events: [EventEmitter | HTMLElement, string, Listener][] = [];
protected events: [EventEmitter | HTMLElement, string, (event: IEvent) => void][] = [];
public destroyed = false;

View File

@ -59,12 +59,13 @@ export class BehaviorController extends ExtensionController<BaseBehavior<CustomB
}
private forwardCanvasEvents = (event: FederatedPointerEvent | FederatedWheelEvent) => {
const target = eventTargetOf(event.target as DisplayObject);
const { target: originalTarget } = event;
const target = eventTargetOf(originalTarget as DisplayObject);
if (!target) return;
const { graph, canvas } = this.context;
const { type: targetType, element: targetElement } = target;
const { type, detail, button } = event;
const stdEvent = { ...event, target: targetElement, targetType };
const stdEvent = { ...event, target: targetElement, targetType, originalTarget };
if (type === CanvasEvent.POINTER_MOVE) {
if (this.currentTarget !== targetElement) {

View File

@ -28,6 +28,7 @@ import type {
ElementDatum,
ElementType,
FitViewOptions,
IEvent,
NodeLikeData,
PartialEdgeData,
PartialGraphData,
@ -1043,4 +1044,28 @@ export class Graph extends EventEmitter {
private onResize = debounce(() => {
this.resize();
}, 300);
/**
* <zh/>
*
* <en/> Listen to events
* @param eventName - <zh/> | <en/> event name
* @param callback - <zh/> | <en/> callback function
* @returns <zh/> Graph | <en/> Graph instance
*/
public on<T extends IEvent = IEvent>(eventName: string, callback: (event: T) => void) {
return super.on(eventName, callback);
}
/**
* <zh/>
*
* <en/> Listen to events once
* @param eventName - <zh/> | <en/> event name
* @param callback - <zh/> | <en/> callback function
* @returns <zh/> Graph | <en/> Graph instance
*/
public once<T extends IEvent = IEvent>(eventName: string, callback: (event: T) => void) {
return super.once(eventName, callback);
}
}

View File

@ -2,32 +2,65 @@ import type {
DisplayObject,
Document,
FederatedEvent,
FederatedMouseEvent,
FederatedPointerEvent,
FederatedWheelEvent,
IAnimation,
} from '@antv/g';
import { Edge, ElementType, Node } from './element';
import type { AnimationType } from '../constants';
import type { ElementDatum } from './data';
import type { Element, ElementType } from './element';
import type { TransformOptions } from './viewport';
export type Listener = (event: any) => void;
export type IEvent =
| IGraphLifeCycleEvent
| IAnimateEvent
| IElementLifeCycleEvent
| IViewportEvent
| IPointerEvent
| IWheelEvent
| IKeyboardEvent
| IDragEvent;
export type Target = Document | Node | Edge | null;
export interface IPointerEvent<T extends Target = Target> extends TargetedEvent<FederatedPointerEvent, T> {}
type BaseEvent<T extends Event | FederatedEvent = Event> = Omit<T, 'target'> & {
targetType: 'canvas' | 'node' | 'edge' | 'combo';
target: DisplayObject;
};
export interface IWheelEvent<T extends Target = Target> extends TargetedEvent<FederatedWheelEvent, T> {}
export interface IElementEvent extends BaseEvent<FederatedMouseEvent> {
targetType: ElementType;
}
export interface IKeyboardEvent extends KeyboardEvent {}
export interface IPointerEvent extends BaseEvent<FederatedPointerEvent> {}
export interface IWheelEvent extends BaseEvent<FederatedWheelEvent> {}
export interface IKeyboardEvent extends BaseEvent<KeyboardEvent> {}
export interface IDragEvent extends IPointerEvent {
export interface IDragEvent<T extends Target = Target> extends TargetedEvent<FederatedPointerEvent, T> {
dx: number;
dy: number;
}
export interface IGraphLifeCycleEvent extends NativeEvent {
data?: any;
}
export interface IElementLifeCycleEvent extends NativeEvent {
elementType: ElementType;
data: ElementDatum;
}
export interface IViewportEvent extends NativeEvent {
data: TransformOptions;
}
export interface IAnimateEvent extends NativeEvent {
animationType: AnimationType;
animation: IAnimation | null;
data?: any;
}
/** <zh/> G6 原生事件 | <en/> G6 native event */
interface NativeEvent {
type: string;
}
/** <zh/> 具有目标的事件 | <en/> Event with target */
type TargetedEvent<E extends FederatedEvent, T extends Target = Target> = Omit<E, 'target'> & {
originalTarget: DisplayObject;
target: T;
targetType: 'canvas' | 'node' | 'edge' | 'combo';
};
export type Target = Document | Element;

View File

@ -3,7 +3,7 @@ import { get, isString } from '@antv/util';
import { BaseCombo, BaseEdge, BaseNode } from '../elements';
import type { NodePortStyleProps } from '../elements/nodes/base-node';
import type { TriangleDirection } from '../elements/nodes/triangle';
import type { Combo, Edge, Node, Placement, Point, Position } from '../types';
import type { Combo, Edge, Element, Node, Placement, Point, Position } from '../types';
import type { LabelPlacement, Port } from '../types/node';
import { getBBoxHeight, getBBoxWidth } from './bbox';
import { isPoint } from './is';
@ -11,38 +11,49 @@ import { findNearestPoints, getEllipseIntersectPoint } from './point';
import { getXYByPlacement } from './position';
/**
* <zh/> BaseNode
* <zh/> Node
*
* <en/> Judge whether the instance is BaseNode
* <en/> Judge whether the instance is Node
* @param shape - <zh/> | <en/> instance
* @returns <zh/> BaseNode | <en/> whether the instance is BaseNode
* @returns <zh/> Node | <en/> whether the instance is Node
*/
export function isNode(shape: DisplayObject | Port): shape is Node {
return shape instanceof BaseNode && shape.type === 'node';
}
/**
* <zh/> BaseEdge
* <zh/> Edge
*
* <en/> Judge whether the instance is BaseEdge
* <en/> Judge whether the instance is Edge
* @param shape - <zh/> | <en/> instance
* @returns <zh/> BaseEdge | <en/> whether the instance is BaseEdge
* @returns <zh/> Edge | <en/> whether the instance is Edge
*/
export function isEdge(shape: DisplayObject): shape is Edge {
return shape instanceof BaseEdge;
}
/**
* <zh/> BaseCombo
* <zh/> Combo
*
* <en/> Judge whether the instance is BaseCombo
* <en/> Judge whether the instance is Combo
* @param shape - <zh/> | <en/> instance
* @returns <zh/> BaseCombo | <en/> whether the instance is BaseCombo
* @returns <zh/> Combo | <en/> whether the instance is Combo
*/
export function isCombo(shape: DisplayObject): shape is Combo {
return shape instanceof BaseCombo;
}
/**
* <zh/> Element
*
* <en/> Judge whether the instance is Element
* @param shape - <zh/> | <en/> instance
* @returns <zh/> Element | <en/> whether the instance is Element
*/
export function isElement(shape: any): shape is Element {
return isNode(shape) || isEdge(shape) || isCombo(shape);
}
/**
* <zh/>
*

View File

@ -1,12 +1,20 @@
import type { IAnimation } from '@antv/g';
import type { AnimationType, GraphEvent } from '../../constants';
import type { ElementDatum, ElementType, TransformOptions } from '../../types';
import type {
ElementDatum,
ElementType,
IAnimateEvent,
IElementLifeCycleEvent,
IGraphLifeCycleEvent,
IViewportEvent,
TransformOptions,
} from '../../types';
export class BaseEvent {
constructor(public type: string) {}
}
export class GraphLifeCycleEvent extends BaseEvent {
export class GraphLifeCycleEvent extends BaseEvent implements IGraphLifeCycleEvent {
constructor(
type:
| GraphEvent.BEFORE_RENDER
@ -23,7 +31,7 @@ export class GraphLifeCycleEvent extends BaseEvent {
}
}
export class AnimateEvent extends BaseEvent {
export class AnimateEvent extends BaseEvent implements IAnimateEvent {
constructor(
type: GraphEvent.BEFORE_ANIMATE | GraphEvent.AFTER_ANIMATE,
public animationType: AnimationType,
@ -34,7 +42,7 @@ export class AnimateEvent extends BaseEvent {
}
}
export class ElementLifeCycleEvent extends BaseEvent {
export class ElementLifeCycleEvent extends BaseEvent implements IElementLifeCycleEvent {
constructor(
type:
| GraphEvent.BEFORE_ELEMENT_CREATE
@ -50,7 +58,7 @@ export class ElementLifeCycleEvent extends BaseEvent {
}
}
export class ViewportEvent extends BaseEvent {
export class ViewportEvent extends BaseEvent implements IViewportEvent {
constructor(
type: GraphEvent.BEFORE_TRANSFORM | GraphEvent.AFTER_TRANSFORM,
public data: TransformOptions,