feat: refactor event controller and graph

This commit is contained in:
zhanning.bzn 2019-12-13 14:40:57 +08:00
parent 274b04fed8
commit 646676f8eb
10 changed files with 615 additions and 35 deletions

View File

@ -14,7 +14,7 @@ module.exports = {
moduleDirectories: [ 'node_modules', 'src' ],
moduleFileExtensions: [ 'js', 'ts', 'json' ],
moduleNameMapper: {
'@g6/(.*)': '<rootDir>/src/$1',
'@g6/types': '<rootDir>/types'
'@g6/types': '<rootDir>/types',
'@g6/(.*)': '<rootDir>/src/$1'
}
};

View File

@ -33,6 +33,7 @@
"dist": "webpack --config webpack.config.js --mode production"
},
"dependencies": {
"@antv/dom-util": "^2.0.1",
"@antv/event-emitter": "~0.1.0",
"@antv/g-base": "^0.1.0-beta.10",
"@antv/g-canvas": "^0.1.0-beta.11",

View File

@ -1,9 +1,10 @@
import each from '@antv/util/lib/each'
import wrapBehavior from '@antv/util/lib/wrap-behavior'
import { IBehavior } from '@g6/interface/behavior';
import { IGraph } from '@g6/interface/graph'
import { G6Event } from '@g6/types';
export default class BehaviorOption {
export default class BehaviorOption implements IBehavior {
private _events = null
private graph = null
private _cfg
@ -21,7 +22,7 @@ export default class BehaviorOption {
}
}
private getDefaultCfg() {
public getDefaultCfg() {
return {
}

View File

@ -0,0 +1,240 @@
import addEventListener from '@antv/dom-util/lib/add-event-listener'
import Canvas from '@antv/g-base/lib/abstract/canvas';
import Group from '@antv/g-canvas/lib/group';
import ShapeBase from '@antv/g-canvas/lib/shape/base';
import each from '@antv/util/lib/each'
import isNil from '@antv/util/lib/is-nil';
import wrapBehavior from '@antv/util/lib/wrap-behavior';
import { IGraph } from '@g6/interface/graph';
import { IItem } from '@g6/interface/item';
import { G6Event, IG6GraphEvent, Matrix } from '@g6/types';
import { cloneEvent } from '@g6/util/base';
interface IEvent {
destroy: () => void;
}
type Fun = () => void
const ORIGIN_MATRIX = [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ];
const MATRIX_LEN = 9;
export default class Event implements IEvent {
private graph: IGraph
private extendEvents: any[]
private canvasHandler: Fun;
private dragging: boolean
private preItem: IItem
constructor(graph: IGraph) {
this.graph = graph
this.extendEvents = []
this.dragging = false
// this.initEvents()
}
// 初始化 G6 中的事件
private initEvents() {
const self = this
const graph = this.graph;
const canvas = graph.get('canvas');
const el = canvas.get('el');
const extendEvents = this.extendEvents;
const canvasHandler: Fun = wrapBehavior(self, 'onCanvasEvents') as Fun;
const originHandler = wrapBehavior(self, 'onExtendEvents');
const wheelHandler = wrapBehavior(self, 'onWheelEvent');
each(G6Event, event => {
canvas.on(event, canvasHandler);
});
this.canvasHandler = canvasHandler;
extendEvents.push(addEventListener(el, 'DOMMouseScroll', wheelHandler));
extendEvents.push(addEventListener(el, 'mousewheel', wheelHandler));
if (typeof window !== 'undefined') {
extendEvents.push(addEventListener(window as any, 'keydown', originHandler));
extendEvents.push(addEventListener(window as any, 'keyup', originHandler));
}
}
// 判断 viewport 是否改变,通过和单位矩阵对比
private isViewportChanged(matrix: Matrix) {
for (let i = 0; i < MATRIX_LEN; i++) {
if (matrix[i] !== ORIGIN_MATRIX[i]) {
return true;
}
}
return false;
}
// 获取 shape 的 item 对象
private getItemRoot<T extends ShapeBase>(shape: any): T {
while (shape && !shape.get('item')) {
shape = shape.get('parent');
}
return shape;
}
/**
* canvas
* @param evt
*/
protected onCanvasEvents(evt: IG6GraphEvent) {
const self = this;
const graph = self.graph;
const canvas: Canvas = graph.get('canvas');
const pixelRatio: number = canvas.get('pixelRatio');
const target = evt.target;
const eventType = evt.type;
/**
* (clientX, clientY):
* (canvasX, canvasY): <canvas>
* (x, y): , model x, y
*/
evt.canvasX = evt.x / pixelRatio;
evt.canvasY = evt.y / pixelRatio;
let point = { x: evt.canvasX, y: evt.canvasY };
const group: Group = graph.get('group')
const matrix: Matrix = group.getMatrix()
if(this.isViewportChanged(matrix)) {
point = graph.getPointByCanvas(evt.canvasX, evt.canvasY)
}
evt.x = point.x
evt.y = point.y
evt.currentTarget = graph
if(target === canvas) {
if (eventType === 'mousemove') {
self.handleMouseMove(evt, 'canvas');
}
evt.target = canvas;
evt.item = null;
graph.emit(eventType, evt);
graph.emit('canvas:' + eventType, evt);
return;
}
const itemShape = this.getItemRoot(target);
if (!itemShape) {
graph.emit(eventType, evt);
return;
}
const item: IItem = itemShape.get('item');
if (item.destroyed) {
return;
}
const type = item.getType();
// 事件target是触发事件的Shape实例, item是触发事件的item实例
evt.target = target;
evt.item = item;
graph.emit(eventType, evt);
// g的事件会冒泡如果target不是canvas可能会引起同个节点触发多次需要另外判断
if (eventType === 'mouseenter' || eventType === 'mouseleave' || eventType === 'dragenter' || eventType === 'dragleave') {
return;
}
graph.emit(type + ':' + eventType, evt);
if (eventType === 'dragstart') {
self.dragging = true;
}
if (eventType === 'dragend') {
self.dragging = false;
}
if (eventType === 'mousemove') {
self.handleMouseMove(evt, type);
}
}
/**
*
* @param evt
*/
protected onExtendEvents(evt: IG6GraphEvent) {
this.graph.emit(evt.type, evt);
}
/**
*
* @param evt
*/
protected onWheelEvent(evt: IG6GraphEvent) {
if (isNil(evt.wheelDelta)) {
evt.wheelDelta = -evt.detail;
}
this.graph.emit('wheel', evt);
}
/**
*
* @param evt
* @param type item
*/
private handleMouseMove(evt: IG6GraphEvent, type: string) {
const self = this;
const graph = this.graph
const canvas: Canvas = graph.get('canvas');
const item = evt.target === canvas ? null : evt.item;
const preItem = this.preItem;
evt = cloneEvent(evt)
// 从前一个item直接移动到当前item触发前一个item的leave事件
if (preItem && preItem !== item && !preItem.destroyed) {
evt.item = preItem;
self.emitCustomEvent(preItem.getType(), 'mouseleave', evt);
if (self.dragging) {
self.emitCustomEvent(preItem.getType(), 'dragleave', evt);
}
}
// 从一个item或canvas移动到当前item触发当前item的enter事件
if (item && preItem !== item) {
evt.item = item;
self.emitCustomEvent(type, 'mouseenter', evt);
if (self.dragging) {
self.emitCustomEvent(type, 'dragenter', evt);
}
}
this.preItem = item;
}
/**
* graph emit
* @param itemType item
* @param eventType
* @param evt
*/
private emitCustomEvent(itemType: string, eventType: string, evt: IG6GraphEvent) {
evt.type = eventType;
this.graph.emit(itemType + ':' + eventType, evt);
}
public destroy() {
const graph = this.graph;
const canvasHandler = this.canvasHandler;
const extendEvents = this.extendEvents
const canvas: Canvas = graph.get('canvas');
each(G6Event, event => {
canvas.off(event, canvasHandler);
});
each(extendEvents, event => {
event.remove();
});
this.dragging = false
this.preItem = null
this.extendEvents.length = 0
this.canvasHandler = null
}
}

View File

@ -1 +1,3 @@
export { default as Mode } from './mode'
export { default as ModeController } from './mode'
export { default as ViewController } from './view'
export { default as EventController } from './event'

View File

@ -1,19 +1,254 @@
import EventEmitter from '@antv/event-emitter'
import { Point } from '@antv/g-base/lib/types';
import GCanvas from '@antv/g-canvas/lib/canvas'
import Group from '@antv/g-canvas/lib/group';
import { mat3 } from '@antv/matrix-util/lib';
import clone from '@antv/util/lib/clone';
import deepMix from '@antv/util/lib/deep-mix'
import isPlainObject from '@antv/util/lib/is-plain-object';
import isString from '@antv/util/lib/is-string'
import { GraphOptions, IGraph } from '@g6/interface/graph';
import { IItem } from '@g6/interface/item';
import { Matrix } from '@g6/types';
import { EdgeConfig, GraphData, GroupConfig, Matrix, NodeConfig, NodeMapConfig } from '@g6/types';
import { translate } from '@g6/util/math'
import { Point } from '_@antv_g-base@0.1.1@@antv/g-base/lib/types';
import Group from '_@antv_g-canvas@0.1.1@@antv/g-canvas/lib/group';
import { mat3 } from '_@antv_matrix-util@2.0.4@@antv/matrix-util/lib';
import clone from '_@antv_util@2.0.6@@antv/util/lib/clone';
import isPlainObject from '_@antv_util@2.0.6@@antv/util/lib/is-plain-object';
import { EventController, ModeController, ViewController } from './controller'
interface PrivateGraphOption extends GraphOptions {
data: GraphData;
// capture event
event: boolean;
nodes: NodeConfig[];
edges: EdgeConfig[];
groups: GroupConfig[];
itemMap: NodeMapConfig;
callback: () => void;
// TODO 需要将暴力出去的配置和内部使用的进行区分
groupBBoxs: any;
groupNodes: NodeMapConfig;
// TODO 需要确定下还是否需要 states
states: any;
}
export default class Graph extends EventEmitter implements IGraph {
private _cfg: GraphOptions
constructor(cfg: GraphOptions) {
super()
this._cfg = cfg
this._cfg = deepMix(this.getDefaultCfg(), cfg)
this.init()
}
private init() {
this.initCanvas()
// instance controller
const eventController = new EventController(this)
const viewController = new ViewController(this)
const modeController = new ModeController(this)
this.set({
eventController,
viewController,
modeController
})
}
private initCanvas() {
let container: string | HTMLElement = this.get('container')
if(isString(container)) {
container = document.getElementById(container)
this.set('container', container)
}
if(!container) {
throw new Error('invalid container')
}
const width: number = this.get('width')
const height: number = this.get('height')
const pixelRatio: number = this.get('pixelRatio')
const canvas = new GCanvas({
container,
width,
height,
pixelRatio
})
this.set('canvas', canvas)
}
public getDefaultCfg(): PrivateGraphOption {
return {
/**
* Container could be dom object or dom id
*/
container: undefined,
/**
* Canvas width
* unit pixel if undefined force fit width
*/
width: undefined,
/**
* Canvas height
* unit pixel if undefined force fit height
*/
height: undefined,
/**
* renderer canvas or svg
*/
renderer: 'canvas',
/**
* control graph behaviors
*/
modes: {},
/**
*
*/
plugins: [],
/**
* source data
*/
data: {},
/**
* Fit view padding (client scale)
*/
fitViewPadding: 10,
/**
* Minimum scale size
*/
minZoom: 0.2,
/**
* Maxmum scale size
*/
maxZoom: 10,
/**
* capture events
*/
event: true,
/**
* group node & edges into different graphic groups
*/
groupByTypes: true,
/**
* determine if it's a directed graph
*/
directed: false,
/**
* when data or shape changed, should canvas draw automatically
*/
autoPaint: true,
/**
* store all the node instances
*/
nodes: [],
/**
* store all the edge instances
*/
edges: [],
/**
* all the instances indexed by id
*/
itemMap: {},
/**
*
*/
linkCenter: false,
/**
* data
* defaultNode: {
* shape: 'rect',
* size: [60, 40],
* style: {
* //... 样式配置项
* }
* }
* { id: 'node', x: 100, y: 100 }
* { id: 'node', x: 100, y: 100 shape: 'rect', size: [60, 40] }
* { id: 'node', x: 100, y: 100, shape: 'circle' }
* { id: 'node', x: 100, y: 100 shape: 'circle', size: [60, 40] }
*/
defaultNode: {},
/**
* data defaultNode
*/
defaultEdge: {},
/**
*
*
* const graph = new G6.Graph({
* nodeStateStyle: {
* selected: { fill: '#ccc', stroke: '#666' },
* active: { lineWidth: 2 }
* },
* ...
* });
*
*/
nodeStateStyles: {},
/**
* nodeStateStyle
*/
edgeStateStyles: {},
/**
* graph
*/
states: {},
/**
*
*/
animate: false,
/**
* , animate true
*/
animateCfg: {
/**
* 线
*/
onFrame: null,
/**
* (ms)
*/
duration: 500,
/**
*
*/
easing: 'easeLinear'
},
callback: null,
/**
* group类型
*/
groupType: 'circle',
/**
* group bbox
* @private
*/
groupBBoxs: {},
/**
* groupid分组的节点数据
* @private
*/
groupNodes: {},
/**
* group
*/
groups: [],
/**
* group样式
*/
groupStyle: {}
};
}
/**
@ -107,4 +342,44 @@ export default class Graph extends EventEmitter implements IGraph {
this.get('canvas').draw();
this.emit('afterpaint');
}
/**
*
* @param {number} clientX x坐标
* @param {number} clientY y坐标
* @return {Point}
*/
public getPointByClient(clientX: number, clientY: number): Point {
return this.get('viewController').getPointByClient(clientX, clientY);
}
/**
*
* @param {number} x x坐标
* @param {number} y y坐标
* @return {Point}
*/
public getClientByPoint(x: number, y: number): Point {
return this.get('viewController').getClientByPoint(x, y);
}
/**
*
* @param {number} canvasX x
* @param {number} canvasY y
* @return {object}
*/
public getPointByCanvas(canvasX, canvasY): Point {
return this.get('viewController').getPointByCanvas(canvasX, canvasY);
}
/**
*
* @param {number} x x
* @param {number} y y
* @return {object}
*/
public getCanvasByPoint(x, y): Point {
return this.get('viewController').getCanvasByPoint(x, y);
}
}

View File

@ -5,20 +5,29 @@ import { IGraph } from './graph';
import { IItem } from './item';
export interface IBehavior {
constructor: (cfg?: object) => void;
// constructor: (cfg?: object) => void;
getEvents: () => { [key in G6Event]?: string };
shouldBegin: () => boolean;
shouldUpdate: () => boolean;
shouldEnd: () => boolean;
bind: (graph: IGraph) => void;
unbind: (graph: IGraph) => void;
[key: string]: (...args: DefaultBehaviorType[]) => unknown;
getDefaultCfg?: () => object;
// [key: string]: (...args: DefaultBehaviorType[]) => unknown;
}
export class G6GraphEvent extends GraphEvent implements IG6GraphEvent {
public item: IItem
public canvasX: number
public canvasY: number
public wheelDelta: number
public detail: number
constructor(type, event) {
super(type, event)
this.item = event.item
this.canvasX = event.canvasX
this.canvasY = event.canvasY
this.wheelDelta = event.wheelDelta
this.detail = event.detail
}
}

View File

@ -11,7 +11,7 @@ export interface IModeOption {
export type IModeType = string | IModeOption
export interface IMode {
default: IModeType[]
default?: IModeType[]
[key: string]: IModeType[]
}
@ -54,8 +54,11 @@ export interface GraphOptions {
*/
groupByTypes?: boolean;
// 是否有向图
directed?: boolean;
groupStyle?: {
style: {
style?: {
[key: string]: ShapeStyle
};
};
@ -145,10 +148,44 @@ export interface GraphOptions {
}
export interface IGraph extends EventEmitter {
getDefaultCfg(): GraphOptions;
get<T = any>(key: string): T;
set<T = any>(key: string | object, value?: T): Graph;
findById(id: string): IItem;
translate(dx: number, dy: number): void;
zoom(ratio: number, center: Point): void;
/**
*
* @param {number} clientX x
* @param {number} clientY y
* @return {Point}
*/
getPointByClient(clientX: number, clientY: number): Point;
/**
*
* @param {number} x x坐标
* @param {number} y y坐标
* @return {object}
*/
getClientByPoint(x: number, y: number): Point;
/**
*
* @param {number} canvasX x
* @param {number} canvasY y
* @return {Point}
*/
getPointByCanvas(canvasX: number, canvasY: number): Point;
/**
*
* @param {number} x x
* @param {number} y y
* @return {Point}
*/
getCanvasByPoint(x: number, y: number): Point;
}

View File

@ -1,25 +1,33 @@
import { Mode } from '../../../../src/graph/controller'
import { ModeController } from '../../../../src/graph/controller'
import Graph from '../../../../src/graph/graph'
import { GraphOptions, IGraph, IModeOption } from '../../../../src/interface/graph';
import { GraphOptions, IModeOption } from '../../../../src/interface/graph';
const div = document.createElement('div');
div.id = 'graph-spec';
document.body.appendChild(div);
describe('Mode Controller', () => {
it('signle mode', () => {
const cfg: GraphOptions = {
container: 'x',
container: 'graph-spec',
width: 200,
height: 100
height: 100,
modes: {
default: ['drag']
}
}
const graph: IGraph = new Graph(cfg)
const modeController = new Mode(graph)
const graph: Graph = new Graph(cfg)
const modeController = new ModeController(graph)
expect(Object.keys(modeController.modes).length).toBe(1);
expect(modeController.modes.default).not.toBe(undefined);
expect(modeController.modes.default.length).toBe(0);
console.log(modeController.modes)
expect(modeController.modes.default[0]).toEqual({ type: 'drag' });
expect(modeController.modes.default.length).toBe(1);
expect(modeController.mode).toBe('default');
})
it('setMode', () => {
const cfg: GraphOptions = {
container: 'x',
container: 'graph-spec',
width: 200,
height: 100,
modes: {
@ -27,8 +35,8 @@ describe('Mode Controller', () => {
edit: ['canvans', 'zoom']
}
}
const graph: IGraph = new Graph(cfg)
const modeController = new Mode(graph)
const graph: Graph = new Graph(cfg)
const modeController = new ModeController(graph)
modeController.setMode('edit')
expect(modeController.modes.edit).not.toBe(undefined)
expect(modeController.modes.edit.length).toBe(2)
@ -37,7 +45,7 @@ describe('Mode Controller', () => {
it('manipulateBehaviors', () => {
const cfg: GraphOptions = {
container: 'x',
container: 'graph-spec',
width: 200,
height: 100,
modes: {
@ -45,8 +53,8 @@ describe('Mode Controller', () => {
edit: ['canvans', 'zoom']
}
}
const graph: IGraph = new Graph(cfg)
const modeController = new Mode(graph)
const graph: Graph = new Graph(cfg)
const modeController = new ModeController(graph)
modeController.manipulateBehaviors(['delete'], 'xxx', true)
expect(Object.keys(modeController.modes).length).toBe(3)
expect(modeController.modes.xxx).not.toBe(undefined)
@ -66,7 +74,7 @@ describe('Mode Controller', () => {
it('add & remove behavior to several modes', () => {
const cfg: GraphOptions = {
container: 'x',
container: 'graph-spec',
width: 500,
height: 500,
pixelRatio: 2,
@ -76,13 +84,12 @@ describe('Mode Controller', () => {
custom2: []
}
}
const graph: IGraph = new Graph(cfg)
const modeController = new Mode(graph)
const graph = new Graph(cfg)
const modeController = new ModeController(graph)
expect(Object.keys(modeController.modes).length).toBe(3);
modeController.manipulateBehaviors([ 'aa', 'bb' ], [ 'custom1', 'custom2' ], true);
expect(modeController.modes.custom1.length).toBe(2);
expect(modeController.modes.custom2.length).toBe(2);
console.log(modeController.modes)
const custom1: IModeOption = modeController.modes.custom1[0] as IModeOption
const custom2: IModeOption = modeController.modes.custom1[1] as IModeOption

View File

@ -76,7 +76,7 @@ export type ModelStyle = Partial<{
}>
export type Easeing =
'easeLinear'
| 'easeLinear'
| 'easePolyIn'
| 'easePolyOut'
| 'easePolyInOut'
@ -110,6 +110,10 @@ export interface EdgeConfig extends ModelConfig {
controlPoints?: IPoint[];
}
export interface NodeMapConfig {
[key: string]: NodeConfig
}
export interface GroupConfig {
id: string;
parentId?: string;
@ -191,4 +195,8 @@ export type IEvent = Record<G6Event, string>
export interface IG6GraphEvent extends GraphEvent {
item: IItem;
canvasX: number;
canvasY: number;
wheelDelta: number;
detail: number;
}