mirror of
https://gitee.com/antv/g6.git
synced 2024-12-02 11:48:29 +08:00
feat: add drag canvas behavior (#5475)
* refactor(types): rename loose to loosen * feat(utils): add Shortcut util * refactor(behaviors): refactor zoom canvas with shortcuts * chore(test): update toBeCloseTo message style * feat(behaviors): add drag-canvas behavior * refactor(utils): abstract out module from behavior and widget * feat(runtime): add widget controller * refactor: fix cr issues
This commit is contained in:
parent
e802349d81
commit
b98a164d5f
36
packages/g6/__tests__/demo/case/behavior-drag-canvas.ts
Normal file
36
packages/g6/__tests__/demo/case/behavior-drag-canvas.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Graph } from '@/src';
|
||||||
|
import data from '@@/dataset/cluster.json';
|
||||||
|
import type { STDTestCase } from '../types';
|
||||||
|
|
||||||
|
export const behaviorDragCanvas: STDTestCase = async (context) => {
|
||||||
|
const { canvas, animation } = context;
|
||||||
|
const graph = new Graph({
|
||||||
|
animation,
|
||||||
|
container: canvas,
|
||||||
|
data,
|
||||||
|
layout: {
|
||||||
|
type: 'd3force',
|
||||||
|
},
|
||||||
|
node: {
|
||||||
|
style: {
|
||||||
|
size: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
behaviors: [
|
||||||
|
'drag-canvas',
|
||||||
|
{
|
||||||
|
type: 'drag-canvas',
|
||||||
|
trigger: {
|
||||||
|
up: ['ArrowUp'],
|
||||||
|
down: ['ArrowDown'],
|
||||||
|
right: ['ArrowRight'],
|
||||||
|
left: ['ArrowLeft'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await graph.render();
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
};
|
@ -1 +1,2 @@
|
|||||||
|
export * from './behavior-drag-canvas';
|
||||||
export * from './behavior-zoom-canvas';
|
export * from './behavior-zoom-canvas';
|
||||||
|
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 66 KiB |
@ -0,0 +1,84 @@
|
|||||||
|
import { CommonEvent, type Graph } from '@/src';
|
||||||
|
import { behaviorDragCanvas } from '@@/demo/case';
|
||||||
|
import { createDemoGraph } from '@@/utils';
|
||||||
|
import { isObject } from '@antv/util';
|
||||||
|
|
||||||
|
describe('behavior drag canvas', () => {
|
||||||
|
let graph: Graph;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
graph = await createDemoGraph(behaviorDragCanvas, { animation: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default status', () => {
|
||||||
|
expect(graph.getBehaviors()).toEqual([
|
||||||
|
'drag-canvas',
|
||||||
|
{
|
||||||
|
type: 'drag-canvas',
|
||||||
|
trigger: {
|
||||||
|
up: ['ArrowUp'],
|
||||||
|
down: ['ArrowDown'],
|
||||||
|
right: ['ArrowRight'],
|
||||||
|
left: ['ArrowLeft'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('arrow up', () => {
|
||||||
|
const [x, y] = graph.getPosition();
|
||||||
|
graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowUp' });
|
||||||
|
graph.emit(CommonEvent.KEY_UP, { key: 'ArrowUp' });
|
||||||
|
|
||||||
|
expect(graph.getPosition()).toBeCloseTo([x, y - 10]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('arrow down', () => {
|
||||||
|
const [x, y] = graph.getPosition();
|
||||||
|
graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowDown' });
|
||||||
|
graph.emit(CommonEvent.KEY_UP, { key: 'ArrowDown' });
|
||||||
|
expect(graph.getPosition()).toBeCloseTo([x, y + 10]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('arrow left', () => {
|
||||||
|
const [x, y] = graph.getPosition();
|
||||||
|
graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowLeft' });
|
||||||
|
graph.emit(CommonEvent.KEY_UP, { key: 'ArrowLeft' });
|
||||||
|
expect(graph.getPosition()).toBeCloseTo([x - 10, y]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('arrow right', () => {
|
||||||
|
const [x, y] = graph.getPosition();
|
||||||
|
graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });
|
||||||
|
graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });
|
||||||
|
expect(graph.getPosition()).toBeCloseTo([x + 10, y]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drag', () => {
|
||||||
|
const [x, y] = graph.getPosition();
|
||||||
|
graph.emit(CommonEvent.DRAG, { movement: { x: 10, y: 10 }, targetType: 'canvas' });
|
||||||
|
expect(graph.getPosition()).toBeCloseTo([x + 10, y + 10]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sensitivity', async () => {
|
||||||
|
graph.setBehaviors((behaviors) =>
|
||||||
|
behaviors.map((behavior) => {
|
||||||
|
if (isObject(behavior)) {
|
||||||
|
return { ...behavior, sensitivity: 20 };
|
||||||
|
}
|
||||||
|
return behavior;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [x, y] = graph.getPosition();
|
||||||
|
graph.emit(CommonEvent.KEY_DOWN, { key: 'ArrowRight' });
|
||||||
|
graph.emit(CommonEvent.KEY_UP, { key: 'ArrowRight' });
|
||||||
|
expect(graph.getPosition()).toBeCloseTo([x + 20, y]);
|
||||||
|
|
||||||
|
await expect(graph.getCanvas()).toMatchSnapshot(__filename);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('destroy', () => {
|
||||||
|
graph.destroy();
|
||||||
|
});
|
||||||
|
});
|
@ -1,26 +0,0 @@
|
|||||||
import { BehaviorOptions } from '@/src';
|
|
||||||
import { parseBehaviors } from '@/src/utils/behaviors';
|
|
||||||
|
|
||||||
describe('behavior', () => {
|
|
||||||
it('parseBehaviors', () => {
|
|
||||||
expect(parseBehaviors([])).toEqual([]);
|
|
||||||
|
|
||||||
const options: BehaviorOptions = [
|
|
||||||
'drag-node',
|
|
||||||
{ type: 'drag-canvas' },
|
|
||||||
{ type: 'shortcut', key: 'shortcut-zoom-in' },
|
|
||||||
{ type: 'shortcut', key: 'shortcut-zoom-out' },
|
|
||||||
'scroll-canvas',
|
|
||||||
'scroll-canvas',
|
|
||||||
];
|
|
||||||
|
|
||||||
expect(parseBehaviors(options)).toEqual([
|
|
||||||
{ type: 'drag-node', key: 'behavior-drag-node-0' },
|
|
||||||
{ type: 'drag-canvas', key: 'behavior-drag-canvas-0' },
|
|
||||||
{ type: 'shortcut', key: 'shortcut-zoom-in' },
|
|
||||||
{ type: 'shortcut', key: 'shortcut-zoom-out' },
|
|
||||||
{ type: 'scroll-canvas', key: 'behavior-scroll-canvas-0' },
|
|
||||||
{ type: 'scroll-canvas', key: 'behavior-scroll-canvas-1' },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
|
54
packages/g6/__tests__/unit/utils/module.spec.ts
Normal file
54
packages/g6/__tests__/unit/utils/module.spec.ts
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { BehaviorOptions, WidgetOptions } from '@/src';
|
||||||
|
import { parseModules } from '@/src/utils/module';
|
||||||
|
|
||||||
|
describe('module', () => {
|
||||||
|
it('parse behavior module', () => {
|
||||||
|
expect(parseModules('behavior', [])).toEqual([]);
|
||||||
|
|
||||||
|
const options: BehaviorOptions = [
|
||||||
|
'drag-node',
|
||||||
|
{ type: 'drag-canvas' },
|
||||||
|
{ type: 'shortcut', key: 'shortcut-zoom-in' },
|
||||||
|
{ type: 'shortcut', key: 'shortcut-zoom-out' },
|
||||||
|
'scroll-canvas',
|
||||||
|
'scroll-canvas',
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(parseModules('behavior', options)).toEqual([
|
||||||
|
{ type: 'drag-node', key: 'behavior-drag-node-0' },
|
||||||
|
{ type: 'drag-canvas', key: 'behavior-drag-canvas-0' },
|
||||||
|
{ type: 'shortcut', key: 'shortcut-zoom-in' },
|
||||||
|
{ type: 'shortcut', key: 'shortcut-zoom-out' },
|
||||||
|
{ type: 'scroll-canvas', key: 'behavior-scroll-canvas-0' },
|
||||||
|
{ type: 'scroll-canvas', key: 'behavior-scroll-canvas-1' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parseWidgets', () => {
|
||||||
|
expect(parseModules('widget', [])).toEqual([]);
|
||||||
|
|
||||||
|
const options: WidgetOptions = [
|
||||||
|
'minimap',
|
||||||
|
{ key: 'my-tooltip', type: 'tooltip' },
|
||||||
|
{ type: 'tooltip' },
|
||||||
|
{
|
||||||
|
type: 'menu',
|
||||||
|
key: 'my-context-menu',
|
||||||
|
trigger: 'contextmenu',
|
||||||
|
},
|
||||||
|
'minimap',
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(parseModules('widget', options)).toEqual([
|
||||||
|
{ type: 'minimap', key: 'widget-minimap-0' },
|
||||||
|
{ type: 'tooltip', key: 'my-tooltip' },
|
||||||
|
{ type: 'tooltip', key: 'widget-tooltip-0' },
|
||||||
|
{
|
||||||
|
type: 'menu',
|
||||||
|
key: 'my-context-menu',
|
||||||
|
trigger: 'contextmenu',
|
||||||
|
},
|
||||||
|
{ type: 'minimap', key: 'widget-minimap-1' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
68
packages/g6/__tests__/unit/utils/shortcut.spec.ts
Normal file
68
packages/g6/__tests__/unit/utils/shortcut.spec.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { CommonEvent } from '@/src';
|
||||||
|
import { Shortcut } from '@/src/utils/shortcut';
|
||||||
|
import EventEmitter from '@antv/event-emitter';
|
||||||
|
|
||||||
|
describe('shortcut', () => {
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
|
const shortcut = new Shortcut(emitter);
|
||||||
|
|
||||||
|
it('bind and unbind', () => {
|
||||||
|
const controlEqual = jest.fn();
|
||||||
|
const controlMinus = jest.fn();
|
||||||
|
shortcut.bind(['Control', '='], controlEqual);
|
||||||
|
shortcut.bind(['Control', '-'], controlMinus);
|
||||||
|
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: '=' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: '=' });
|
||||||
|
|
||||||
|
expect(controlEqual).toHaveBeenCalledTimes(1);
|
||||||
|
expect(controlMinus).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: '-' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: '-' });
|
||||||
|
|
||||||
|
expect(controlEqual).toHaveBeenCalledTimes(1);
|
||||||
|
expect(controlMinus).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: '=' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: '=' });
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: '-' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: '-' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });
|
||||||
|
|
||||||
|
expect(controlEqual).toHaveBeenCalledTimes(2);
|
||||||
|
expect(controlMinus).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
shortcut.unbind(['Control', '='], controlEqual);
|
||||||
|
shortcut.unbind(['Control', '-']);
|
||||||
|
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: '=' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: '=' });
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: '-' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: '-' });
|
||||||
|
emitter.emit(CommonEvent.KEY_UP, { key: 'Control' });
|
||||||
|
|
||||||
|
expect(controlEqual).toHaveBeenCalledTimes(2);
|
||||||
|
expect(controlMinus).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wheel', () => {
|
||||||
|
const wheel = jest.fn();
|
||||||
|
shortcut.bind(['Control', 'wheel'], wheel);
|
||||||
|
|
||||||
|
emitter.emit(CommonEvent.WHEEL, { deltaX: 0, deltaY: 10 });
|
||||||
|
expect(wheel).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
emitter.emit(CommonEvent.KEY_DOWN, { key: 'Control' });
|
||||||
|
emitter.emit(CommonEvent.WHEEL, { deltaX: 0, deltaY: 10 });
|
||||||
|
expect(wheel).toHaveBeenCalledTimes(1);
|
||||||
|
expect(wheel.mock.calls[0][0].deltaY).toBe(10);
|
||||||
|
});
|
||||||
|
});
|
@ -33,16 +33,9 @@ declare global {
|
|||||||
expect.extend({
|
expect.extend({
|
||||||
toBeCloseTo: (received: Digital, expected: Digital, numDigits?: number) => {
|
toBeCloseTo: (received: Digital, expected: Digital, numDigits?: number) => {
|
||||||
const pass = toBeCloseTo(received, expected, numDigits);
|
const pass = toBeCloseTo(received, expected, numDigits);
|
||||||
if (pass) {
|
return {
|
||||||
return {
|
message: () => `expected: \x1b[32m${received}\n\x1b[31mreceived: ${expected}\x1b[0m`,
|
||||||
message: () => `expected ${received} not to be close to ${expected}`,
|
pass,
|
||||||
pass: true,
|
};
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
message: () => `expected ${received} to be close to ${expected}`,
|
|
||||||
pass: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,49 +1,6 @@
|
|||||||
import type EventEmitter from '@antv/event-emitter';
|
|
||||||
import type { RuntimeContext } from '../runtime/types';
|
|
||||||
import type { CustomBehaviorOption } from '../spec/behavior';
|
import type { CustomBehaviorOption } from '../spec/behavior';
|
||||||
import type { Listener } from '../types';
|
import { BaseModule } from '../utils/module';
|
||||||
|
|
||||||
export type BaseBehaviorOptions = CustomBehaviorOption;
|
export type BaseBehaviorOptions = CustomBehaviorOption;
|
||||||
|
|
||||||
export abstract class BaseBehavior<T extends BaseBehaviorOptions> {
|
export abstract class BaseBehavior<T extends BaseBehaviorOptions> extends BaseModule<T> {}
|
||||||
protected context: RuntimeContext;
|
|
||||||
|
|
||||||
protected options: Required<T>;
|
|
||||||
|
|
||||||
public events: [EventEmitter | HTMLElement, string, Listener][] = [];
|
|
||||||
|
|
||||||
public destroyed = false;
|
|
||||||
|
|
||||||
public get defaultOptions(): Partial<T> {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: RuntimeContext, options: T) {
|
|
||||||
this.context = context;
|
|
||||||
this.options = Object.assign({}, this.defaultOptions, options) as Required<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(options: Partial<T>) {
|
|
||||||
this.options = Object.assign(this.options, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
public addEventListener(emitter: EventEmitter | HTMLElement, eventName: string, listener: Listener) {
|
|
||||||
if (emitter instanceof HTMLElement) emitter.addEventListener(eventName, listener);
|
|
||||||
else emitter.on(eventName, listener);
|
|
||||||
this.events.push([emitter, eventName, listener]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy() {
|
|
||||||
this.events.forEach(([emitter, event, listener]) => {
|
|
||||||
if (emitter instanceof HTMLElement) emitter.removeEventListener(event, listener);
|
|
||||||
else emitter.off(event, listener);
|
|
||||||
});
|
|
||||||
|
|
||||||
// @ts-expect-error force delete
|
|
||||||
delete this.context;
|
|
||||||
// @ts-expect-error force delete
|
|
||||||
delete this.options;
|
|
||||||
|
|
||||||
this.destroyed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
121
packages/g6/src/behaviors/drag-canvas.ts
Normal file
121
packages/g6/src/behaviors/drag-canvas.ts
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import type { Cursor, FederatedMouseEvent } from '@antv/g';
|
||||||
|
import { isFunction, isObject } from '@antv/util';
|
||||||
|
import { CanvasEvent } from '../constants';
|
||||||
|
import { RuntimeContext } from '../runtime/types';
|
||||||
|
import type { BehaviorEvent, Point, ViewportAnimationEffectTiming } from '../types';
|
||||||
|
import type { ShortcutKey } from '../utils/shortcut';
|
||||||
|
import { Shortcut } from '../utils/shortcut';
|
||||||
|
import { multiply } from '../utils/vector';
|
||||||
|
import type { BaseBehaviorOptions } from './base-behavior';
|
||||||
|
import { BaseBehavior } from './base-behavior';
|
||||||
|
|
||||||
|
export interface DragCanvasOptions extends BaseBehaviorOptions {
|
||||||
|
/**
|
||||||
|
* <zh/> 是否启用缩放动画,仅在使用按键移动时有效
|
||||||
|
*
|
||||||
|
* <en/> Whether to enable the animation of zooming, only valid when using key movement
|
||||||
|
*/
|
||||||
|
animation?: ViewportAnimationEffectTiming;
|
||||||
|
/**
|
||||||
|
* <zh/> 是否启用拖拽画布的功能
|
||||||
|
*
|
||||||
|
* <en/> Whether to enable the function of dragging the canvas
|
||||||
|
*/
|
||||||
|
enable?: boolean | ((event: BehaviorEvent<FederatedMouseEvent> | BehaviorEvent<KeyboardEvent>) => boolean);
|
||||||
|
/**
|
||||||
|
* <zh/> 触发拖拽的方式,默认使用指针按下拖拽
|
||||||
|
*
|
||||||
|
* <en/> The way to trigger dragging, default to dragging with the pointer pressed
|
||||||
|
*/
|
||||||
|
trigger?: CombinationKey;
|
||||||
|
/**
|
||||||
|
* <zh/> 触发一次按键移动的距离
|
||||||
|
*
|
||||||
|
* <en/> The distance of a single key movement
|
||||||
|
*/
|
||||||
|
sensitivity?: number;
|
||||||
|
/**
|
||||||
|
* <zh/> 完成拖拽时的回调
|
||||||
|
*
|
||||||
|
* <en/> Callback when dragging is completed
|
||||||
|
*/
|
||||||
|
onfinish?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CombinationKey = {
|
||||||
|
up: ShortcutKey;
|
||||||
|
down: ShortcutKey;
|
||||||
|
left: ShortcutKey;
|
||||||
|
right: ShortcutKey;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class DragCanvas extends BaseBehavior<DragCanvasOptions> {
|
||||||
|
static defaultOptions: Partial<DragCanvasOptions> = {
|
||||||
|
enable: true,
|
||||||
|
sensitivity: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
private shortcut: Shortcut;
|
||||||
|
|
||||||
|
private defaultCursor: Cursor;
|
||||||
|
|
||||||
|
private get animation() {
|
||||||
|
return this.context.options.animation ? this.options.animation : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: RuntimeContext, options: DragCanvasOptions) {
|
||||||
|
super(context, Object.assign({}, DragCanvas.defaultOptions, options));
|
||||||
|
|
||||||
|
this.shortcut = new Shortcut(context.graph);
|
||||||
|
|
||||||
|
this.bindEvents();
|
||||||
|
this.defaultCursor = this.context.canvas.getConfig().cursor || 'default';
|
||||||
|
context.canvas.setCursor('grab');
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindEvents() {
|
||||||
|
const { trigger } = this.options;
|
||||||
|
this.shortcut.unbindAll();
|
||||||
|
const { graph } = this.context;
|
||||||
|
|
||||||
|
if (isObject(trigger)) {
|
||||||
|
graph.off(CanvasEvent.DRAG, this.onDrag);
|
||||||
|
const { up = [], down = [], left = [], right = [] } = trigger;
|
||||||
|
|
||||||
|
this.shortcut.bind(up, (event) => this.translate([0, 1], event));
|
||||||
|
this.shortcut.bind(down, (event) => this.translate([0, -1], event));
|
||||||
|
this.shortcut.bind(left, (event) => this.translate([1, 0], event));
|
||||||
|
this.shortcut.bind(right, (event) => this.translate([-1, 0], event));
|
||||||
|
} else {
|
||||||
|
graph.on(CanvasEvent.DRAG, this.onDrag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDrag = (event: BehaviorEvent<FederatedMouseEvent>) => {
|
||||||
|
if (event.targetType === 'canvas') {
|
||||||
|
this.context.viewport?.translate(
|
||||||
|
{ mode: 'relative', value: [event.movement.x, event.movement.y] },
|
||||||
|
this.animation,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private translate(value: Point, event: BehaviorEvent<FederatedMouseEvent> | BehaviorEvent<KeyboardEvent>) {
|
||||||
|
if (!this.validate(event)) return;
|
||||||
|
const { sensitivity } = this.options;
|
||||||
|
const delta = sensitivity * -1;
|
||||||
|
this.context.viewport?.translate({ mode: 'relative', value: multiply(value, [delta, delta]) }, this.animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validate(event: BehaviorEvent<FederatedMouseEvent> | BehaviorEvent<KeyboardEvent>) {
|
||||||
|
if (this.destroyed) return false;
|
||||||
|
const { enable } = this.options;
|
||||||
|
if (isFunction(enable)) return enable(event);
|
||||||
|
return !!enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy(): void {
|
||||||
|
this.context.canvas.setCursor(this.defaultCursor);
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import type { ZoomCanvasOptions } from './zoom-canvas';
|
import type { ZoomCanvasOptions } from './zoom-canvas';
|
||||||
|
|
||||||
export { BaseBehavior } from './base-behavior';
|
export { BaseBehavior } from './base-behavior';
|
||||||
|
export { DragCanvas } from './drag-canvas';
|
||||||
export { ZoomCanvas } from './zoom-canvas';
|
export { ZoomCanvas } from './zoom-canvas';
|
||||||
|
|
||||||
export type { BaseBehaviorOptions } from './base-behavior';
|
export type { BaseBehaviorOptions } from './base-behavior';
|
||||||
|
export type { DragCanvasOptions } from './drag-canvas';
|
||||||
export type BuiltInBehaviorOptions = ZoomCanvasOptions;
|
export type BuiltInBehaviorOptions = ZoomCanvasOptions;
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import type { BaseBehavior } from './base-behavior';
|
import type { BaseBehavior } from './base-behavior';
|
||||||
|
import type { DragCanvasOptions } from './drag-canvas';
|
||||||
|
import type { ZoomCanvasOptions } from './zoom-canvas';
|
||||||
|
|
||||||
export type BuiltInBehaviorOptions = { type: 'unset' };
|
export type BuiltInBehaviorOptions = DragCanvasOptions | ZoomCanvasOptions;
|
||||||
export type Behavior = BaseBehavior<any>;
|
export type Behavior = BaseBehavior<any>;
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { isArray, isEqual, isObject } from '@antv/util';
|
import { isArray, isFunction, isObject } from '@antv/util';
|
||||||
import { CommonEvent } from '../constants';
|
import { CanvasEvent } from '../constants';
|
||||||
import type { RuntimeContext } from '../runtime/types';
|
import type { RuntimeContext } from '../runtime/types';
|
||||||
import type { BehaviorEvent, Loose, ViewportAnimationEffectTiming } from '../types';
|
import type { BehaviorEvent, ViewportAnimationEffectTiming } from '../types';
|
||||||
|
import type { ShortcutKey } from '../utils/shortcut';
|
||||||
|
import { Shortcut } from '../utils/shortcut';
|
||||||
import type { BaseBehaviorOptions } from './base-behavior';
|
import type { BaseBehaviorOptions } from './base-behavior';
|
||||||
import { BaseBehavior } from './base-behavior';
|
import { BaseBehavior } from './base-behavior';
|
||||||
|
|
||||||
@ -17,23 +19,21 @@ export interface ZoomCanvasOptions extends BaseBehaviorOptions {
|
|||||||
*
|
*
|
||||||
* <en/> Whether to enable the function of zooming the canvas
|
* <en/> Whether to enable the function of zooming the canvas
|
||||||
*/
|
*/
|
||||||
enable?: boolean | ((event: BehaviorEvent) => boolean);
|
enable?: boolean | ((event: BehaviorEvent<WheelEvent> | BehaviorEvent<KeyboardEvent>) => boolean);
|
||||||
/**
|
/**
|
||||||
* <zh/> 触发缩放的方式
|
* <zh/> 触发缩放的方式
|
||||||
*
|
*
|
||||||
* <en/> The way to trigger zoom
|
* <en/> The way to trigger zoom
|
||||||
* @description
|
* @description
|
||||||
* <zh/>
|
* <zh/>
|
||||||
* - 'wheel':滚动鼠标滚轮或触摸板时触发缩放
|
* - 数组:组合快捷键,默认使用滚轮缩放,['Control'] 表示按住 Control 键滚动鼠标滚轮时触发缩放
|
||||||
* - 数组:组合快捷键,例如 ['ctrl'] 表示按住 ctrl 键滚动鼠标滚轮时触发缩放
|
* - 对象:缩放快捷键,例如 { zoomIn: ['Control', '+'], zoomOut: ['Control', '-'], reset: ['Control', '0'] }
|
||||||
* - 对象:缩放快捷键,例如 { zoomIn: ['ctrl', '+'], zoomOut: ['ctrl', '-'], reset: ['ctrl', '0'] }
|
|
||||||
*
|
*
|
||||||
* <en/>
|
* <en/>
|
||||||
* - 'wheel': Trigger zoom when scrolling the mouse wheel or touchpad
|
* - Array: Combination shortcut key, default to zoom in and out with the mouse wheel, ['Control'] means zooming when holding down the Control key and scrolling the mouse wheel
|
||||||
* - Array: Combination shortcut keys, such as ['ctrl'] means zooming when scrolling the mouse wheel while holding down the ctrl key
|
* - Object: Zoom shortcut key, such as { zoomIn: ['Control', '+'], zoomOut: ['Control', '-'], reset: ['Control', '0'] }
|
||||||
* - Object: Zoom shortcut keys, such as { zoomIn: ['ctrl', '+'], zoomOut: ['ctrl', '-'], reset: ['ctrl', '0'] }
|
|
||||||
*/
|
*/
|
||||||
trigger?: Loose<CommonEvent.WHEEL> | string[] | CombinationKey;
|
trigger?: ShortcutKey | CombinationKey;
|
||||||
/**
|
/**
|
||||||
* <zh/> 缩放灵敏度
|
* <zh/> 缩放灵敏度
|
||||||
*
|
*
|
||||||
@ -49,86 +49,62 @@ export interface ZoomCanvasOptions extends BaseBehaviorOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CombinationKey = {
|
type CombinationKey = {
|
||||||
zoomIn: string[];
|
zoomIn: ShortcutKey;
|
||||||
zoomOut: string[];
|
zoomOut: ShortcutKey;
|
||||||
reset: string[];
|
reset: ShortcutKey;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ZoomCanvas extends BaseBehavior<ZoomCanvasOptions> {
|
export class ZoomCanvas extends BaseBehavior<ZoomCanvasOptions> {
|
||||||
private preconditionKey?: string[];
|
static defaultOptions: Partial<ZoomCanvasOptions> = {
|
||||||
|
animation: { duration: 200 },
|
||||||
private recordKey = new Set<string>();
|
enable: true,
|
||||||
|
sensitivity: 1,
|
||||||
private combinationKey: CombinationKey = {
|
trigger: [],
|
||||||
zoomIn: [],
|
|
||||||
zoomOut: [],
|
|
||||||
reset: [],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private shortcut: Shortcut;
|
||||||
|
|
||||||
private get animation() {
|
private get animation() {
|
||||||
return this.context.options.animation ? this.options.animation : false;
|
return this.context.options.animation ? this.options.animation : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get defaultOptions(): Partial<ZoomCanvasOptions> {
|
|
||||||
return { animation: { duration: 200 }, enable: true, sensitivity: 1, trigger: CommonEvent.WHEEL };
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(context: RuntimeContext, options: ZoomCanvasOptions) {
|
constructor(context: RuntimeContext, options: ZoomCanvasOptions) {
|
||||||
super(context, options);
|
super(context, Object.assign({}, ZoomCanvas.defaultOptions, options));
|
||||||
|
|
||||||
if (isArray(this.options.trigger)) {
|
this.shortcut = new Shortcut(context.graph);
|
||||||
this.preconditionKey = this.options.trigger;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isObject(this.options.trigger)) {
|
|
||||||
this.combinationKey = this.options.trigger as CombinationKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bindEvents();
|
this.bindEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
private bindEvents() {
|
public update(options: Partial<ZoomCanvasOptions>): void {
|
||||||
const { graph } = this.context;
|
super.update(options);
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
// wheel 触发和组合键触发需要监听 wheel 事件 / Combination key trigger and wheel trigger need to listen to the wheel event
|
private bindEvents() {
|
||||||
if (this.options.trigger === CommonEvent.WHEEL || isArray(this.options.trigger)) {
|
const { trigger } = this.options;
|
||||||
this.preventDefault(CommonEvent.WHEEL);
|
this.shortcut.unbindAll();
|
||||||
this.addEventListener(graph, CommonEvent.WHEEL, this.onWheel.bind(this));
|
|
||||||
|
if (isArray(trigger)) {
|
||||||
|
if (trigger.includes(CanvasEvent.WHEEL)) {
|
||||||
|
this.preventDefault(CanvasEvent.WHEEL);
|
||||||
|
}
|
||||||
|
this.shortcut.bind([...trigger, CanvasEvent.WHEEL], this.onWheel);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isObject(this.options.trigger)) {
|
if (isObject(trigger)) {
|
||||||
this.addEventListener(graph, CommonEvent.KEY_DOWN, this.onKeydown.bind(this));
|
const { zoomIn = [], zoomOut = [], reset = [] } = trigger as CombinationKey;
|
||||||
this.addEventListener(graph, CommonEvent.KEY_UP, this.onKeyup.bind(this));
|
this.shortcut.bind(zoomIn, (event) => this.zoom(1, event));
|
||||||
|
this.shortcut.bind(zoomOut, (event) => this.zoom(-1, event));
|
||||||
|
this.shortcut.bind(reset, this.onReset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onWheel(event: BehaviorEvent<WheelEvent>) {
|
private onWheel = (event: BehaviorEvent<WheelEvent>) => {
|
||||||
const { deltaX, deltaY } = event;
|
const { deltaX, deltaY } = event;
|
||||||
const delta = -(deltaY || deltaX);
|
const delta = -(deltaY || deltaX);
|
||||||
this.zoom(delta, event);
|
this.zoom(delta, event);
|
||||||
}
|
};
|
||||||
|
|
||||||
private onKeydown(event: BehaviorEvent<KeyboardEvent>) {
|
|
||||||
const { key } = event;
|
|
||||||
this.recordKey.add(key);
|
|
||||||
|
|
||||||
if (this.isTrigger(this.combinationKey.zoomIn)) {
|
|
||||||
this.zoom(1, event);
|
|
||||||
} else if (this.isTrigger(this.combinationKey.zoomOut)) {
|
|
||||||
this.zoom(-1, event);
|
|
||||||
} else if (this.isTrigger(this.combinationKey.reset)) {
|
|
||||||
this.reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onKeyup(event: KeyboardEvent) {
|
|
||||||
const { key } = event;
|
|
||||||
this.recordKey.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isTrigger(keys: string[]) {
|
|
||||||
return isEqual(Array.from(this.recordKey), keys);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <zh/> 缩放画布
|
* <zh/> 缩放画布
|
||||||
@ -137,7 +113,7 @@ export class ZoomCanvas extends BaseBehavior<ZoomCanvasOptions> {
|
|||||||
* @param value - <zh/> 缩放值, > 0 放大, < 0 缩小 | <en/> Zoom value, > 0 zoom in, < 0 zoom out
|
* @param value - <zh/> 缩放值, > 0 放大, < 0 缩小 | <en/> Zoom value, > 0 zoom in, < 0 zoom out
|
||||||
* @param event - <zh/> 事件对象 | <en/> Event object
|
* @param event - <zh/> 事件对象 | <en/> Event object
|
||||||
*/
|
*/
|
||||||
private async zoom(value: number, event: BehaviorEvent<WheelEvent> | BehaviorEvent<KeyboardEvent>) {
|
private zoom = async (value: number, event: BehaviorEvent<WheelEvent> | BehaviorEvent<KeyboardEvent>) => {
|
||||||
if (!this.validate(event)) return;
|
if (!this.validate(event)) return;
|
||||||
const { viewport } = this.context;
|
const { viewport } = this.context;
|
||||||
if (!viewport) return;
|
if (!viewport) return;
|
||||||
@ -148,26 +124,24 @@ export class ZoomCanvas extends BaseBehavior<ZoomCanvasOptions> {
|
|||||||
await viewport.zoom({ mode: 'absolute', value: zoom + diff }, this.animation);
|
await viewport.zoom({ mode: 'absolute', value: zoom + diff }, this.animation);
|
||||||
|
|
||||||
onfinish?.();
|
onfinish?.();
|
||||||
}
|
};
|
||||||
|
|
||||||
private async reset() {
|
private onReset = async () => {
|
||||||
const { viewport } = this.context;
|
const { viewport } = this.context;
|
||||||
await viewport?.zoom({ mode: 'absolute', value: 1 }, this.animation);
|
await viewport?.zoom({ mode: 'absolute', value: 1 }, this.animation);
|
||||||
}
|
};
|
||||||
|
|
||||||
private validate(event: BehaviorEvent<WheelEvent> | BehaviorEvent<KeyboardEvent>) {
|
private validate(event: BehaviorEvent<WheelEvent> | BehaviorEvent<KeyboardEvent>) {
|
||||||
if (this.preconditionKey && !isEqual(this.preconditionKey, Array.from(this.recordKey))) return false;
|
if (this.destroyed) return false;
|
||||||
|
|
||||||
const { enable } = this.options;
|
const { enable } = this.options;
|
||||||
if (typeof enable === 'function' && !enable(event)) return false;
|
if (isFunction(enable)) return enable(event);
|
||||||
if (enable === false) return false;
|
return !!enable;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private preventDefault(eventName: string) {
|
private preventDefault(eventName: string) {
|
||||||
const listener = (e: Event) => e.preventDefault();
|
const listener = (e: Event) => e.preventDefault();
|
||||||
const container = this.context.canvas.getContainer();
|
const container = this.context.canvas.getContainer();
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
this.addEventListener(container, eventName, listener);
|
container.addEventListener(eventName, listener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export enum CanvasEvent {
|
export const enum CanvasEvent {
|
||||||
/** <zh/> 点击时触发 | <en/> Triggered when click */
|
/** <zh/> 点击时触发 | <en/> Triggered when click */
|
||||||
CLICK = 'click',
|
CLICK = 'click',
|
||||||
/** <zh/> 双击时触发 | <en/> Triggered when double click */
|
/** <zh/> 双击时触发 | <en/> Triggered when double click */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export enum CommonEvent {
|
export const enum CommonEvent {
|
||||||
/** <zh/> 点击时触发 | <en/> Triggered when click */
|
/** <zh/> 点击时触发 | <en/> Triggered when click */
|
||||||
CLICK = 'click',
|
CLICK = 'click',
|
||||||
/** <zh/> 双击时触发 | <en/> Triggered when double click */
|
/** <zh/> 双击时触发 | <en/> Triggered when double click */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export enum ContainerEvent {
|
export const enum ContainerEvent {
|
||||||
/** <zh/> 按下键盘时触发 | <en/> Triggered when the keyboard is pressed */
|
/** <zh/> 按下键盘时触发 | <en/> Triggered when the keyboard is pressed */
|
||||||
KEY_DOWN = 'keydown',
|
KEY_DOWN = 'keydown',
|
||||||
/** <zh/> 抬起键盘时触发 | <en/> Triggered when the keyboard is lifted */
|
/** <zh/> 抬起键盘时触发 | <en/> Triggered when the keyboard is lifted */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export enum EdgeEvent {
|
export const enum EdgeEvent {
|
||||||
/** <zh/> 点击时触发 | <en/> Triggered when click */
|
/** <zh/> 点击时触发 | <en/> Triggered when click */
|
||||||
CLICK = 'click',
|
CLICK = 'click',
|
||||||
/** <zh/> 双击时触发 | <en/> Triggered when double click */
|
/** <zh/> 双击时触发 | <en/> Triggered when double click */
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export enum NodeEvent {
|
export const enum NodeEvent {
|
||||||
/** <zh/> 点击时触发 | <en/> Triggered when click */
|
/** <zh/> 点击时触发 | <en/> Triggered when click */
|
||||||
CLICK = 'click',
|
CLICK = 'click',
|
||||||
/** <zh/> 双击时触发 | <en/> Triggered when double click */
|
/** <zh/> 双击时触发 | <en/> Triggered when double click */
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { fade, translate } from '../animations';
|
import { fade, translate } from '../animations';
|
||||||
import { ZoomCanvas } from '../behaviors';
|
import { DragCanvas, ZoomCanvas } from '../behaviors';
|
||||||
import {
|
import {
|
||||||
Circle,
|
Circle,
|
||||||
Cubic,
|
Cubic,
|
||||||
@ -46,6 +46,7 @@ export const BUILT_IN_PLUGINS = {
|
|||||||
},
|
},
|
||||||
behavior: {
|
behavior: {
|
||||||
'zoom-canvas': ZoomCanvas,
|
'zoom-canvas': ZoomCanvas,
|
||||||
|
'drag-canvas': DragCanvas,
|
||||||
},
|
},
|
||||||
combo: {},
|
combo: {},
|
||||||
edge: {
|
edge: {
|
||||||
|
@ -4,10 +4,10 @@ import type { Behavior } from '../behaviors/types';
|
|||||||
import type { STDPalette } from '../palettes/types';
|
import type { STDPalette } from '../palettes/types';
|
||||||
import type { Theme } from '../themes/types';
|
import type { Theme } from '../themes/types';
|
||||||
import type { Edge, Node } from '../types';
|
import type { Edge, Node } from '../types';
|
||||||
|
import type { Widget } from '../widgets/types';
|
||||||
|
|
||||||
// TODO 待使用正式类型定义 / To be used formal type definition
|
// TODO 待使用正式类型定义 / To be used formal type definition
|
||||||
declare type Combo = unknown;
|
declare type Combo = unknown;
|
||||||
declare type Widget = unknown;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <zh/> 插件注册表
|
* <zh/> 插件注册表
|
||||||
|
@ -1,88 +1,58 @@
|
|||||||
import type { DisplayObject, FederatedPointerEvent, FederatedWheelEvent } from '@antv/g';
|
import type { DisplayObject, FederatedPointerEvent, FederatedWheelEvent } from '@antv/g';
|
||||||
import type { BaseBehavior } from '../behaviors/base-behavior';
|
import type { BaseBehavior } from '../behaviors/base-behavior';
|
||||||
import { CanvasEvent, ContainerEvent } from '../constants';
|
import { CanvasEvent, ContainerEvent } from '../constants';
|
||||||
import { getPlugin } from '../registry';
|
import type { BehaviorOptions, CustomBehaviorOption } from '../spec/behavior';
|
||||||
import type { BehaviorOptions } from '../spec';
|
|
||||||
import type { STDBehaviorOption } from '../spec/behavior';
|
|
||||||
import type { Target } from '../types';
|
import type { Target } from '../types';
|
||||||
import { parseBehaviors } from '../utils/behaviors';
|
|
||||||
import { arrayDiff } from '../utils/diff';
|
|
||||||
import { eventTargetOf } from '../utils/event';
|
import { eventTargetOf } from '../utils/event';
|
||||||
|
import { ModuleController } from '../utils/module';
|
||||||
import type { RuntimeContext } from './types';
|
import type { RuntimeContext } from './types';
|
||||||
|
|
||||||
export class BehaviorController {
|
export class BehaviorController extends ModuleController<BaseBehavior<CustomBehaviorOption>> {
|
||||||
private context: RuntimeContext;
|
|
||||||
|
|
||||||
private behaviors: STDBehaviorOption[] = [];
|
|
||||||
|
|
||||||
private behaviorMap: Record<string, BaseBehavior<any>> = {};
|
|
||||||
|
|
||||||
/** <zh/> 当前事件的目标 | <en/> The current event target */
|
/** <zh/> 当前事件的目标 | <en/> The current event target */
|
||||||
private currentTarget: Target | null = null;
|
private currentTarget: Target | null = null;
|
||||||
|
|
||||||
|
public category: 'widget' | 'behavior' = 'behavior';
|
||||||
|
|
||||||
constructor(context: RuntimeContext) {
|
constructor(context: RuntimeContext) {
|
||||||
this.context = context;
|
super(context);
|
||||||
this.forwardEvents();
|
this.forwardEvents();
|
||||||
this.setBehaviors(this.context.options?.behaviors || []);
|
this.setBehaviors(this.context.options.behaviors || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setBehaviors(behaviors: BehaviorOptions) {
|
public setBehaviors(behaviors: BehaviorOptions) {
|
||||||
const newBehaviors = parseBehaviors(behaviors);
|
this.setModules(behaviors);
|
||||||
const { enter, update, exit, keep } = arrayDiff(this.behaviors, newBehaviors, (behavior) => behavior.key);
|
|
||||||
|
|
||||||
this.createBehaviors(enter);
|
|
||||||
this.updateBehaviors([...update, ...keep]);
|
|
||||||
this.destroyBehaviors(exit);
|
|
||||||
|
|
||||||
this.behaviors = newBehaviors;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createBehavior(behavior: STDBehaviorOption) {
|
|
||||||
const { key, type } = behavior;
|
|
||||||
const Ctor = getPlugin('behavior', type);
|
|
||||||
if (!Ctor) return;
|
|
||||||
|
|
||||||
const instance = new Ctor(this.context, behavior);
|
|
||||||
this.behaviorMap[key] = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
private createBehaviors(behaviors: STDBehaviorOption[]) {
|
|
||||||
behaviors.forEach((behavior) => this.createBehavior(behavior));
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateBehaviors(behaviors: STDBehaviorOption[]) {
|
|
||||||
behaviors.forEach((behavior) => {
|
|
||||||
const { key } = behavior;
|
|
||||||
const instance = this.behaviorMap[key];
|
|
||||||
if (instance) {
|
|
||||||
instance.update(behavior);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private destroyBehavior(key: string) {
|
|
||||||
const instance = this.behaviorMap[key];
|
|
||||||
if (instance) {
|
|
||||||
instance.destroy();
|
|
||||||
delete this.behaviorMap[key];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private destroyBehaviors(behaviors: STDBehaviorOption[]) {
|
|
||||||
behaviors.forEach(({ key }) => this.destroyBehavior(key));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private forwardEvents() {
|
private forwardEvents() {
|
||||||
const container = this.context.canvas.getContainer();
|
const container = this.context.canvas.getContainer();
|
||||||
if (container) {
|
if (container) {
|
||||||
Object.values(ContainerEvent).forEach((name) => {
|
[ContainerEvent.KEY_DOWN, ContainerEvent.KEY_UP].forEach((name) => {
|
||||||
container.addEventListener(name, this.forwardContainerEvents.bind(this));
|
container.addEventListener(name, this.forwardContainerEvents.bind(this));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = this.context.canvas.document;
|
const canvas = this.context.canvas.document;
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
Object.values(CanvasEvent).forEach((name) => {
|
[
|
||||||
|
CanvasEvent.CLICK,
|
||||||
|
CanvasEvent.DBLCLICK,
|
||||||
|
CanvasEvent.POINTER_OVER,
|
||||||
|
CanvasEvent.POINTER_LEAVE,
|
||||||
|
CanvasEvent.POINTER_ENTER,
|
||||||
|
CanvasEvent.POINTER_MOVE,
|
||||||
|
CanvasEvent.POINTER_OUT,
|
||||||
|
CanvasEvent.POINTER_DOWN,
|
||||||
|
CanvasEvent.POINTER_UP,
|
||||||
|
CanvasEvent.CONTEXT_MENU,
|
||||||
|
CanvasEvent.DRAG_START,
|
||||||
|
CanvasEvent.DRAG,
|
||||||
|
CanvasEvent.DRAG_END,
|
||||||
|
CanvasEvent.DRAG_ENTER,
|
||||||
|
CanvasEvent.DRAG_OVER,
|
||||||
|
CanvasEvent.DRAG_LEAVE,
|
||||||
|
CanvasEvent.DROP,
|
||||||
|
CanvasEvent.WHEEL,
|
||||||
|
].forEach((name) => {
|
||||||
canvas.addEventListener(name, this.forwardCanvasEvents.bind(this));
|
canvas.addEventListener(name, this.forwardCanvasEvents.bind(this));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -138,14 +108,4 @@ export class BehaviorController {
|
|||||||
private forwardContainerEvents(event: FocusEvent | KeyboardEvent) {
|
private forwardContainerEvents(event: FocusEvent | KeyboardEvent) {
|
||||||
this.context.graph.emit(event.type, event);
|
this.context.graph.emit(event.type, event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy() {
|
|
||||||
Object.keys(this.behaviorMap).forEach((key) => this.destroyBehavior(key));
|
|
||||||
// @ts-expect-error force delete
|
|
||||||
delete this.context;
|
|
||||||
// @ts-expect-error force delete
|
|
||||||
delete this.behaviors;
|
|
||||||
// @ts-expect-error force delete
|
|
||||||
delete this.behaviorMap;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@ import { ElementController } from './element';
|
|||||||
import { LayoutController } from './layout';
|
import { LayoutController } from './layout';
|
||||||
import { RuntimeContext } from './types';
|
import { RuntimeContext } from './types';
|
||||||
import { ViewportController } from './viewport';
|
import { ViewportController } from './viewport';
|
||||||
|
import { WidgetController } from './widget';
|
||||||
|
|
||||||
export class Graph extends EventEmitter {
|
export class Graph extends EventEmitter {
|
||||||
private options: G6Spec;
|
private options: G6Spec;
|
||||||
@ -163,8 +164,9 @@ export class Graph extends EventEmitter {
|
|||||||
return this.options.behaviors || [];
|
return this.options.behaviors || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public setWidgets(widgets: CallableValue<G6Spec['widgets']>): void {
|
public setWidgets(widgets: CallableValue<WidgetOptions>): void {
|
||||||
this.options.widgets = isFunction(widgets) ? widgets(this.getWidgets()) : widgets;
|
this.options.widgets = isFunction(widgets) ? widgets(this.getWidgets()) : widgets;
|
||||||
|
this.context.widget?.setWidgets(this.options.widgets);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWidgets(): WidgetOptions {
|
public getWidgets(): WidgetOptions {
|
||||||
@ -309,6 +311,7 @@ export class Graph extends EventEmitter {
|
|||||||
|
|
||||||
private createRuntime() {
|
private createRuntime() {
|
||||||
this.context.options = this.options;
|
this.context.options = this.options;
|
||||||
|
if (!this.context.widget) this.context.widget = new WidgetController(this.context);
|
||||||
if (!this.context.viewport) this.context.viewport = new ViewportController(this.context);
|
if (!this.context.viewport) this.context.viewport = new ViewportController(this.context);
|
||||||
if (!this.context.element) this.context.element = new ElementController(this.context);
|
if (!this.context.element) this.context.element = new ElementController(this.context);
|
||||||
if (!this.context.layout) this.context.layout = new LayoutController(this.context);
|
if (!this.context.layout) this.context.layout = new LayoutController(this.context);
|
||||||
@ -366,12 +369,13 @@ export class Graph extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
const { layout, element, model, canvas, behavior } = this.context;
|
const { layout, element, model, canvas, behavior, widget } = this.context;
|
||||||
layout?.destroy();
|
layout?.destroy();
|
||||||
element?.destroy();
|
element?.destroy();
|
||||||
model.destroy();
|
model.destroy();
|
||||||
canvas?.destroy();
|
canvas?.destroy();
|
||||||
behavior?.destroy();
|
behavior?.destroy();
|
||||||
|
widget?.destroy();
|
||||||
this.options = {};
|
this.options = {};
|
||||||
// @ts-expect-error force delete
|
// @ts-expect-error force delete
|
||||||
delete this.context;
|
delete this.context;
|
||||||
|
@ -6,6 +6,7 @@ import type { ElementController } from './element';
|
|||||||
import type { Graph } from './graph';
|
import type { Graph } from './graph';
|
||||||
import type { LayoutController } from './layout';
|
import type { LayoutController } from './layout';
|
||||||
import type { ViewportController } from './viewport';
|
import type { ViewportController } from './viewport';
|
||||||
|
import type { WidgetController } from './widget';
|
||||||
|
|
||||||
export interface RuntimeContext {
|
export interface RuntimeContext {
|
||||||
/**
|
/**
|
||||||
@ -60,4 +61,10 @@ export interface RuntimeContext {
|
|||||||
* <en/> Behavior controller
|
* <en/> Behavior controller
|
||||||
*/
|
*/
|
||||||
behavior?: BehaviorController;
|
behavior?: BehaviorController;
|
||||||
|
/**
|
||||||
|
* <zh/> 组件控制器
|
||||||
|
*
|
||||||
|
* <en/> Widget controller
|
||||||
|
*/
|
||||||
|
widget?: WidgetController;
|
||||||
}
|
}
|
||||||
|
17
packages/g6/src/runtime/widget.ts
Normal file
17
packages/g6/src/runtime/widget.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import type { CustomWidgetOption, WidgetOptions } from '../spec/widget';
|
||||||
|
import { ModuleController } from '../utils/module';
|
||||||
|
import type { BaseWidget } from '../widgets/base-widget';
|
||||||
|
import type { RuntimeContext } from './types';
|
||||||
|
|
||||||
|
export class WidgetController extends ModuleController<BaseWidget<CustomWidgetOption>> {
|
||||||
|
public category: 'widget' | 'behavior' = 'widget';
|
||||||
|
|
||||||
|
constructor(context: RuntimeContext) {
|
||||||
|
super(context);
|
||||||
|
this.setWidgets(this.context.options.widgets || []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setWidgets(widgets: WidgetOptions) {
|
||||||
|
this.setModules(widgets);
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
import type { BuiltInBehaviorOptions } from '../behaviors';
|
import type { BuiltInBehaviorOptions } from '../behaviors';
|
||||||
|
import type { LooselyModuleOption, ModuleOptions, STDModuleOption } from '../types';
|
||||||
|
|
||||||
export type BehaviorOptions = Abbr<BuiltInBehaviorOptions | CustomBehaviorOption>[];
|
export type BehaviorOptions = ModuleOptions<BuiltInBehaviorOptions>;
|
||||||
|
|
||||||
export type STDBehaviorOption = { type: string; key: string; [key: string]: unknown };
|
export type STDBehaviorOption = STDModuleOption<BuiltInBehaviorOptions>;
|
||||||
|
|
||||||
export type CustomBehaviorOption = { type: string; key?: string; [key: string]: unknown };
|
export type CustomBehaviorOption = LooselyModuleOption;
|
||||||
|
|
||||||
type Abbr<R extends CustomBehaviorOption> = (R & { key?: string }) | R['type'];
|
|
||||||
|
@ -1,12 +1,8 @@
|
|||||||
|
import type { LooselyModuleOption, ModuleOptions, STDModuleOption } from '../types';
|
||||||
import type { BuiltInWidgetOptions } from '../widgets/types';
|
import type { BuiltInWidgetOptions } from '../widgets/types';
|
||||||
|
|
||||||
export type WidgetOptions = Abbr<BuiltInWidgetOptions | CustomWidgetOptions>[];
|
export type WidgetOptions = ModuleOptions<BuiltInWidgetOptions>;
|
||||||
|
|
||||||
type CustomWidgetOptions = STDWidgetOptions;
|
export type STDWidgetOption = STDModuleOption<BuiltInWidgetOptions>;
|
||||||
|
|
||||||
export interface STDWidgetOptions {
|
export type CustomWidgetOption = LooselyModuleOption;
|
||||||
type: string;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Abbr<R extends STDWidgetOptions> = (R & { key?: string }) | R['type'];
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
export type BehaviorEvent<T extends Event = Event> = T & {
|
import type { FederatedEvent } from '@antv/g';
|
||||||
|
|
||||||
|
export type BehaviorEvent<T extends Event | FederatedEvent = Event> = T & {
|
||||||
targetType: 'canvas' | 'node' | 'edge' | 'combo';
|
targetType: 'canvas' | 'node' | 'edge' | 'combo';
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ChangeTypeEnum } from '../constants';
|
import { ChangeTypeEnum } from '../constants';
|
||||||
import type { ComboData, EdgeData, NodeData } from '../spec/data';
|
import type { ComboData, EdgeData, NodeData } from '../spec/data';
|
||||||
import { Loose } from './enum';
|
import { Loosen } from './enum';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <zh/> 数据变更
|
* <zh/> 数据变更
|
||||||
@ -16,49 +16,49 @@ export type DataUpdated = NodeUpdated | EdgeUpdated | ComboUpdated;
|
|||||||
export type DataRemoved = NodeRemoved | EdgeRemoved | ComboRemoved;
|
export type DataRemoved = NodeRemoved | EdgeRemoved | ComboRemoved;
|
||||||
|
|
||||||
export type NodeAdded = {
|
export type NodeAdded = {
|
||||||
type: Loose<ChangeTypeEnum.NodeAdded>;
|
type: Loosen<ChangeTypeEnum.NodeAdded>;
|
||||||
value: NodeData;
|
value: NodeData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeUpdated = {
|
export type NodeUpdated = {
|
||||||
type: Loose<ChangeTypeEnum.NodeUpdated>;
|
type: Loosen<ChangeTypeEnum.NodeUpdated>;
|
||||||
value: NodeData;
|
value: NodeData;
|
||||||
original: NodeData;
|
original: NodeData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type NodeRemoved = {
|
export type NodeRemoved = {
|
||||||
type: Loose<ChangeTypeEnum.NodeRemoved>;
|
type: Loosen<ChangeTypeEnum.NodeRemoved>;
|
||||||
value: NodeData;
|
value: NodeData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EdgeAdded = {
|
export type EdgeAdded = {
|
||||||
type: Loose<ChangeTypeEnum.EdgeAdded>;
|
type: Loosen<ChangeTypeEnum.EdgeAdded>;
|
||||||
value: EdgeData;
|
value: EdgeData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EdgeUpdated = {
|
export type EdgeUpdated = {
|
||||||
type: Loose<ChangeTypeEnum.EdgeUpdated>;
|
type: Loosen<ChangeTypeEnum.EdgeUpdated>;
|
||||||
value: EdgeData;
|
value: EdgeData;
|
||||||
original: EdgeData;
|
original: EdgeData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type EdgeRemoved = {
|
export type EdgeRemoved = {
|
||||||
type: Loose<ChangeTypeEnum.EdgeRemoved>;
|
type: Loosen<ChangeTypeEnum.EdgeRemoved>;
|
||||||
value: EdgeData;
|
value: EdgeData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ComboAdded = {
|
export type ComboAdded = {
|
||||||
type: Loose<ChangeTypeEnum.ComboAdded>;
|
type: Loosen<ChangeTypeEnum.ComboAdded>;
|
||||||
value: ComboData;
|
value: ComboData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ComboUpdated = {
|
export type ComboUpdated = {
|
||||||
type: Loose<ChangeTypeEnum.ComboUpdated>;
|
type: Loosen<ChangeTypeEnum.ComboUpdated>;
|
||||||
value: ComboData;
|
value: ComboData;
|
||||||
original: ComboData;
|
original: ComboData;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ComboRemoved = {
|
export type ComboRemoved = {
|
||||||
type: Loose<ChangeTypeEnum.ComboRemoved>;
|
type: Loosen<ChangeTypeEnum.ComboRemoved>;
|
||||||
value: ComboData;
|
value: ComboData;
|
||||||
};
|
};
|
||||||
|
@ -1 +1 @@
|
|||||||
export type Loose<T extends string> = `${T}`;
|
export type Loosen<T extends string> = `${T}`;
|
||||||
|
@ -10,6 +10,7 @@ export type * from './enum';
|
|||||||
export type * from './event';
|
export type * from './event';
|
||||||
export type * from './graphlib';
|
export type * from './graphlib';
|
||||||
export type * from './layout';
|
export type * from './layout';
|
||||||
|
export type * from './module';
|
||||||
export type * from './node';
|
export type * from './node';
|
||||||
export type * from './padding';
|
export type * from './padding';
|
||||||
export type * from './point';
|
export type * from './point';
|
||||||
|
44
packages/g6/src/types/module.ts
Normal file
44
packages/g6/src/types/module.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* <zh/> 模块配置项
|
||||||
|
*
|
||||||
|
* <en/> Module options
|
||||||
|
*/
|
||||||
|
export type ModuleOptions<Registry extends Record<string, unknown> = Record<string, unknown>> = ModuleOption<
|
||||||
|
Registry | LooselyModuleOption
|
||||||
|
>[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <zh/> 模块配置项
|
||||||
|
*
|
||||||
|
* <en/> Module option
|
||||||
|
*/
|
||||||
|
type ModuleOption<Registry extends Record<string, unknown> = Record<string, unknown>> = AbbrModuleOption<
|
||||||
|
LooselyModuleOption<Registry>
|
||||||
|
>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <zh/> 标准模块配置项
|
||||||
|
*
|
||||||
|
* <en/> Standard module options
|
||||||
|
*/
|
||||||
|
export type STDModuleOption<Registry extends Record<string, unknown> = Record<string, unknown>> = {
|
||||||
|
type: string;
|
||||||
|
key: string;
|
||||||
|
} & Registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <zh/> 宽松的模块配置项,可以不传入 key
|
||||||
|
*
|
||||||
|
* <en/> Loosely module option, key can be omitted
|
||||||
|
*/
|
||||||
|
export type LooselyModuleOption<Registry extends Record<string, unknown> = Record<string, unknown>> = {
|
||||||
|
type: string;
|
||||||
|
key?: string;
|
||||||
|
} & Registry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <zh/> 模块配置项简写,支持直接传入 type 字符串
|
||||||
|
*
|
||||||
|
* <en/> Module option abbreviation, support directly passing in type string
|
||||||
|
*/
|
||||||
|
type AbbrModuleOption<S extends LooselyModuleOption> = (S & { key?: string }) | S['type'];
|
@ -1,24 +0,0 @@
|
|||||||
import type { BehaviorOptions } from '../spec';
|
|
||||||
import type { STDBehaviorOption } from '../spec/behavior';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <zh/> 将行为配置项转换为标准格式
|
|
||||||
*
|
|
||||||
* <en/> Convert behavior options to standard format
|
|
||||||
* @param behaviors - <zh/> 行为配置项 <en/> behavior options
|
|
||||||
* @returns <zh/> 标准行为配置项 <en/> Standard behavior options
|
|
||||||
*/
|
|
||||||
export function parseBehaviors(behaviors: BehaviorOptions): STDBehaviorOption[] {
|
|
||||||
const counter: Record<string, number> = {};
|
|
||||||
const getKey = (type: string) => {
|
|
||||||
if (!(type in counter)) counter[type] = 0;
|
|
||||||
return `behavior-${type}-${counter[type]++}`;
|
|
||||||
};
|
|
||||||
return behaviors.map((behavior) => {
|
|
||||||
if (typeof behavior === 'string') {
|
|
||||||
return { type: behavior, key: getKey(behavior) };
|
|
||||||
}
|
|
||||||
if (behavior.key) return behavior as STDBehaviorOption;
|
|
||||||
return { ...behavior, key: getKey(behavior.type) };
|
|
||||||
});
|
|
||||||
}
|
|
147
packages/g6/src/utils/module.ts
Normal file
147
packages/g6/src/utils/module.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* @file module.ts
|
||||||
|
* @description
|
||||||
|
* <zh/> Behavior 和 Widget 作为可插拔的模块,具有较高的相似性,因此将其抽象为`模块`
|
||||||
|
*
|
||||||
|
* <en/> Behavior and Widget are pluggable modules with high similarity, so they are abstracted as `module`
|
||||||
|
*/
|
||||||
|
import type EventEmitter from '@antv/event-emitter';
|
||||||
|
import { getPlugin } from '../registry';
|
||||||
|
import type { RuntimeContext } from '../runtime/types';
|
||||||
|
import type { Listener, LooselyModuleOption, ModuleOptions, STDModuleOption } from '../types';
|
||||||
|
import { arrayDiff } from './diff';
|
||||||
|
|
||||||
|
export abstract class ModuleController<Module extends BaseModule<LooselyModuleOption>> {
|
||||||
|
protected context: RuntimeContext;
|
||||||
|
|
||||||
|
protected modules: STDModuleOption[] = [];
|
||||||
|
|
||||||
|
protected moduleMap: Record<string, Module> = {};
|
||||||
|
|
||||||
|
public abstract category: 'widget' | 'behavior';
|
||||||
|
|
||||||
|
constructor(context: RuntimeContext) {
|
||||||
|
this.context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setModules(modules: ModuleOptions) {
|
||||||
|
const stdModules = parseModules(this.category, modules) as STDModuleOption[];
|
||||||
|
const { enter, update, exit, keep } = arrayDiff(this.modules, stdModules, (module) => module.key);
|
||||||
|
|
||||||
|
this.createModules(enter);
|
||||||
|
this.updateModules([...update, ...keep]);
|
||||||
|
this.destroyModules(exit);
|
||||||
|
|
||||||
|
this.modules = stdModules;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createModule(module: STDModuleOption) {
|
||||||
|
const { category } = this;
|
||||||
|
|
||||||
|
const { key, type } = module;
|
||||||
|
const Ctor = getPlugin(category as any, type);
|
||||||
|
if (!Ctor) return;
|
||||||
|
|
||||||
|
const instance = new Ctor(this.context, module);
|
||||||
|
this.moduleMap[key] = instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createModules(modules: STDModuleOption[]) {
|
||||||
|
modules.forEach((module) => this.createModule(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateModule(module: STDModuleOption) {
|
||||||
|
const { key } = module;
|
||||||
|
const instance = this.moduleMap[key];
|
||||||
|
if (instance) {
|
||||||
|
instance.update(module);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateModules(modules: STDModuleOption[]) {
|
||||||
|
modules.forEach((module) => this.updateModule(module));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected destroyModule(key: string) {
|
||||||
|
const instance = this.moduleMap[key];
|
||||||
|
if (instance) {
|
||||||
|
instance.destroy();
|
||||||
|
delete this.moduleMap[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected destroyModules(modules: STDModuleOption[]) {
|
||||||
|
modules.forEach(({ key }) => this.destroyModule(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
Object.values(this.moduleMap).forEach((module) => module.destroy());
|
||||||
|
// @ts-expect-error force delete
|
||||||
|
delete this.context;
|
||||||
|
// @ts-expect-error force delete
|
||||||
|
delete this.modules;
|
||||||
|
// @ts-expect-error force delete
|
||||||
|
delete this.moduleMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <zh/> 模块实例基类
|
||||||
|
*
|
||||||
|
* <en/> Base class for module instance
|
||||||
|
*/
|
||||||
|
export class BaseModule<T extends LooselyModuleOption> {
|
||||||
|
protected context: RuntimeContext;
|
||||||
|
|
||||||
|
protected options: Required<T>;
|
||||||
|
|
||||||
|
protected events: [EventEmitter | HTMLElement, string, Listener][] = [];
|
||||||
|
|
||||||
|
public destroyed = false;
|
||||||
|
|
||||||
|
constructor(context: RuntimeContext, options: T) {
|
||||||
|
this.context = context;
|
||||||
|
this.options = options as Required<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(options: Partial<T>) {
|
||||||
|
this.options = Object.assign(this.options, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
// @ts-expect-error force delete
|
||||||
|
delete this.context;
|
||||||
|
// @ts-expect-error force delete
|
||||||
|
delete this.options;
|
||||||
|
|
||||||
|
this.destroyed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <zh/> 将模块配置项转换为标准模块格式
|
||||||
|
*
|
||||||
|
* <en/> Convert module options to standard format
|
||||||
|
* @param category - <zh/> 模块类型 <en/> module type
|
||||||
|
* @param modules - <zh/> 模块配置项 <en/> module options
|
||||||
|
* @returns <zh/> 标准模块配置项 <en/> Standard module options
|
||||||
|
*/
|
||||||
|
export function parseModules<T extends Record<string, unknown>>(
|
||||||
|
category: 'widget' | 'behavior',
|
||||||
|
modules: ModuleOptions<T>,
|
||||||
|
): STDModuleOption<T>[] {
|
||||||
|
const counter: Record<string, number> = {};
|
||||||
|
|
||||||
|
const getKey = (type: string) => {
|
||||||
|
if (!(type in counter)) counter[type] = 0;
|
||||||
|
return `${category}-${type}-${counter[type]++}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return modules.map((module) => {
|
||||||
|
if (typeof module === 'string') {
|
||||||
|
return { type: module, key: getKey(module) };
|
||||||
|
}
|
||||||
|
if (module.key) return module;
|
||||||
|
return { ...module, key: getKey(module.type) };
|
||||||
|
}) as STDModuleOption<T>[];
|
||||||
|
}
|
77
packages/g6/src/utils/shortcut.ts
Normal file
77
packages/g6/src/utils/shortcut.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import EventEmitter from '@antv/event-emitter';
|
||||||
|
import { isEqual } from '@antv/util';
|
||||||
|
import { CommonEvent } from '../constants';
|
||||||
|
|
||||||
|
export interface ShortcutOptions {}
|
||||||
|
|
||||||
|
export type ShortcutKey = string[];
|
||||||
|
|
||||||
|
type Handler = (event: any) => void;
|
||||||
|
|
||||||
|
export class Shortcut {
|
||||||
|
private map: Map<ShortcutKey, Handler> = new Map();
|
||||||
|
|
||||||
|
private emitter: EventEmitter;
|
||||||
|
|
||||||
|
private recordKey = new Set<string>();
|
||||||
|
|
||||||
|
constructor(emitter: EventEmitter) {
|
||||||
|
this.emitter = emitter;
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bind(key: ShortcutKey, handler: Handler) {
|
||||||
|
if (key.length === 0) return;
|
||||||
|
this.map.set(key, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unbind(key: ShortcutKey, handler?: Handler) {
|
||||||
|
this.map.forEach((h, k) => {
|
||||||
|
if (isEqual(k, key)) {
|
||||||
|
if (!handler || handler === h) this.map.delete(k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public unbindAll() {
|
||||||
|
this.map.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindEvents() {
|
||||||
|
const { emitter } = this;
|
||||||
|
|
||||||
|
emitter.on(CommonEvent.KEY_DOWN, this.onKeyDown);
|
||||||
|
emitter.on(CommonEvent.KEY_UP, this.onKeyUp);
|
||||||
|
emitter.on(CommonEvent.WHEEL, this.onWheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
this.recordKey.add(event.key);
|
||||||
|
this.trigger(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onKeyUp = (event: KeyboardEvent) => {
|
||||||
|
this.recordKey.delete(event.key);
|
||||||
|
};
|
||||||
|
|
||||||
|
private trigger(event: KeyboardEvent) {
|
||||||
|
this.map.forEach((handler, key) => {
|
||||||
|
if (isEqual(Array.from(this.recordKey), key)) handler(event);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private onWheel = (event: WheelEvent) => {
|
||||||
|
this.map.forEach((handler, key) => {
|
||||||
|
if (key.includes(CommonEvent.WHEEL)) {
|
||||||
|
if (
|
||||||
|
isEqual(
|
||||||
|
Array.from(this.recordKey),
|
||||||
|
key.filter((k) => k !== CommonEvent.WHEEL),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
handler(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
6
packages/g6/src/widgets/base-widget.ts
Normal file
6
packages/g6/src/widgets/base-widget.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { CustomWidgetOption } from '../spec/widget';
|
||||||
|
import { BaseModule } from '../utils/module';
|
||||||
|
|
||||||
|
export type BaseWidgetOptions = CustomWidgetOption;
|
||||||
|
|
||||||
|
export abstract class BaseWidget<T extends BaseWidgetOptions> extends BaseModule<T> {}
|
@ -1 +1,4 @@
|
|||||||
|
import type { BaseWidget } from './base-widget';
|
||||||
|
|
||||||
export type BuiltInWidgetOptions = { type: 'unset' };
|
export type BuiltInWidgetOptions = { type: 'unset' };
|
||||||
|
export type Widget = BaseWidget<any>;
|
||||||
|
Loading…
Reference in New Issue
Block a user