feat: add behavior controller and click-select (#4328)

* feat: add behavior controller and click-select

* fix: fix code review

---------

Co-authored-by: 14 <yisi.wys@antgroup.com>
This commit is contained in:
14 2023-03-02 20:27:14 +08:00 committed by GitHub
parent 0f23a181db
commit 2d9cdb8f41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 601 additions and 104 deletions

View File

@ -44,6 +44,7 @@
"prettier": "prettier -c --write \"**/*\"",
"test": "jest",
"test-live": "DEBUG_MODE=1 jest --watch ./tests/unit/item-spec.ts",
"test-behavior": "DEBUG_MODE=1 jest --watch ./tests/unit/click-select-spec.ts",
"lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx",
"watch": "father build -w"
},
@ -69,7 +70,7 @@
"typedoc-plugin-markdown": "^3.14.0"
},
"devDependencies": {
"@types/jest": "^25.2.1",
"@types/jest": "^29.4.0",
"@types/node": "13.11.1",
"@typescript-eslint/eslint-plugin": "^4.11.1",
"@umijs/fabric": "^2.0.0",
@ -92,4 +93,4 @@
"typedoc": "^0.23.24",
"typescript": "^4.6.3"
}
}
}

View File

@ -60,6 +60,8 @@ export default abstract class Item implements IItem {
public init(props) {
const { model, containerGroup, mapper, stateMapper, renderExtensions} = props;
this.group = new Group();
this.group.setAttribute('data-item-type', this.type);
this.group.setAttribute('data-item-id', props.model.id);
containerGroup.appendChild(this.group);
this.model = model;
this.mapper = mapper;
@ -331,7 +333,7 @@ export default abstract class Item implements IItem {
/**
* Re-draw the item with merged state styles.
* @param previousStates previous states
* @returns
* @returns
*/
private drawWithStates(previousStates: State[]) {
if (!this.stateMapper) return;
@ -395,7 +397,7 @@ export default abstract class Item implements IItem {
// diffData
undefined,
// diffState
{
{
previous: previousStates,
current: this.states,
}

View File

@ -22,11 +22,13 @@ export default class Node extends Item {
this.draw(this.displayModel as NodeDisplayModel);
}
public draw(displayModel: NodeDisplayModel, diffData?: { previous: NodeModelData; current: NodeModelData }, diffState?: { previous: State[], current: State[] }) {
const { group, renderExt, shapeMap: prevShapeMap } = this;
const { group, renderExt, shapeMap: prevShapeMap, model } = this;
const { data } = displayModel;
const { x = 0, y = 0 } = data;
group.style.x = x;
group.style.y = y;
this.group.setAttribute('data-item-type', 'node');
this.group.setAttribute('data-item-id', model.id);
const shapeMap = renderExt.draw(displayModel, this.shapeMap, diffData, diffState);
// add shapes to group, and update shapeMap

View File

@ -1,95 +1,316 @@
import { isObject } from '@antv/util';
import { registry } from '../../stdlib';
import { IGraph } from '../../types';
import { getExtension } from '../../util/extension';
import { IElement } from '@antv/g';
import { IGraph } from "../../types";
import { registery } from '../../stdlib';
import { getExtension } from "../../util/extension";
import { Behavior } from "../../types/behavior";
import { CANVAS_EVENT_TYPE, DOM_EVENT_TYPE, IG6GraphEvent } from '../../types/event';
import { getItemInfoFromElement, ItemInfo } from '../../util/event';
type Listener = (event: IG6GraphEvent) => void;
/**
* Wraps the listener with error logging.
* @returns a new listener with error logging.
*/
const wrapListener = (type: string, eventName: string, listener: Listener): Listener => {
return (event: any) => {
try {
listener(event);
} catch (error) {
console.error(`G6: Error occurred in "${eventName}" phase of the behavior "${type}"!`)
throw error;
}
};
}
/**
* Manages the interaction extensions and graph modes;
* Storage related data.
*/
export class InteractionController {
public extensions = {};
public graph: IGraph;
public mode: string;
private graph: IGraph;
private mode: string;
/**
* Available behaviors of current mode.
* @example
* { 'drag-node': DragNode, 'drag-canvas': DragCanvas }
*/
private behaviorMap: Map<string, Behavior> = new Map();
/**
* Listeners added by all current behaviors.
* @example
* {
* 'drag-node': { 'node:pointerEnter': function },
* }
*/
private listenersMap: Record<string, Record<string, Listener>> = {};
private prevItemInfo: ItemInfo;
constructor(graph: IGraph<any>) {
this.graph = graph;
this.initEvents();
this.tap();
}
/**
* Subscribe the lifecycle of graph.
*/
private tap() {
this.extensions = this.getExtensions();
this.graph.hooks.init.tap(() => this.onModeChange(this, { mode: 'default' }));
this.graph.hooks.modechange.tap((...params) => this.onModeChange(this, ...params));
this.graph.hooks.behaviorchange.tap((...params) => this.onBehaviorChange(this, ...params));
private tap = (): void => {
this.graph.hooks.init.tap(() => this.onModeChange({ mode: 'default' }));
this.onModeChange({ mode: 'default' });
this.graph.hooks.modechange.tap(this.onModeChange);
this.graph.hooks.behaviorchange.tap(this.onBehaviorChange);
}
/**
* Get the extensions from useLib, stdLib is a sub set of useLib.
* @returns
*/
private getExtensions() {
const { modes = {} } = this.graph.getSpecification();
const modeBehaviors = {};
Object.keys(modes).forEach((mode) => {
modeBehaviors[mode] = modes[mode]
.map((config) => getExtension(config, registry.useLib, 'behavior'))
.filter((behavior) => !!behavior);
private validateMode = (mode: string): boolean => {
if (mode === 'default') return true;
const modes = this.graph.getSpecification().modes || {};
return Object.keys(modes).includes(mode);
}
private initBehavior = (config: string | { type: string }): Behavior | null => {
const type = typeof config === 'string' ? config : (config as any).type;
if (this.behaviorMap.has(type)) {
console.error(`G6: Failed to add behavior "${type}"! It was already added.`);
return;
}
try {
// Get behavior extensions from useLib.
const BehaviorClass = getExtension(config, registery.useLib, 'behavior');
const options = typeof config === 'string' ? {} : config;
const behavior = new BehaviorClass(options);
behavior.graph = this.graph;
if (behavior) {
this.behaviorMap.set(type, behavior);
}
return behavior;
} catch (error) {
console.error(`G6: Failed to initialize behavior "${type}"!`, error);
return null;
}
}
private destroyBehavior = (type: string, behavior: Behavior) => {
try {
behavior.destroy();
} catch (error) {
console.error(`G6: Failed to destroy behavior "${type}"!`, error);
}
}
private addListeners = (type: string, behavior: Behavior) => {
const events = behavior.getEvents();
this.listenersMap[type] = {};
Object.keys(events).forEach(eventName => {
// Wrap the listener with error logging.
const listener = wrapListener(type, eventName, events[eventName]);
this.graph.on(eventName, listener);
this.listenersMap[type][eventName] = listener;
});
}
private removeListeners = (type: string) => {
Object.keys(this.listenersMap[type] || {}).forEach(eventName => {
const listener = this.listenersMap[type][eventName];
this.graph.off(eventName, listener);
});
return modeBehaviors;
}
/**
* Listener of graph's init hook. Add listeners from behaviors to graph.
* @param param contains the mode to switch to
*/
private onModeChange(self, param: { mode: string }) {
self.mode = param.mode;
// TODO: add listeners from behaviors in mode
// ...
private onModeChange = (param: { mode: string }) => {
const { mode } = param;
// Skip if set to same mode.
if (this.mode === mode) {
return;
}
if (!this.validateMode(mode)) {
console.warn(`G6: Mode "${mode}" was not specified in current graph.`);
}
this.mode = mode;
// 1. Remove listeners && destroy current behaviors.
this.behaviorMap.forEach((behavior, type) => {
this.removeListeners(type);
this.destroyBehavior(type, behavior);
});
// 2. Initialize new behaviors.
this.behaviorMap.clear();
const behaviorConfigs = this.graph.getSpecification().modes?.[mode] || [];
behaviorConfigs.forEach(config => {
this.initBehavior(config);
});
// 3. Add listeners for each behavior.
this.listenersMap = {};
this.behaviorMap.forEach((behavior, type) => {
this.addListeners(type, behavior);
});
}
/**
* Listener of graph's behaviorchange hook. Update, add, or remove behaviors from modes.
* @param param contains action, modes, and behaviors
*/
private onBehaviorChange(
self,
param: {
action: 'update' | 'add' | 'remove';
modes: string[];
behaviors: (string | { key: string; type: string })[];
},
) {
private onBehaviorChange = (param: {
action: 'update' | 'add' | 'remove',
modes: string[],
behaviors: (string | { type: string })[],
}) => {
const { action, modes, behaviors } = param;
modes.forEach((mode) => {
switch (action) {
case 'add':
behaviors.forEach((config) =>
self.extensions[mode].push(getExtension(config, registry.useLib, 'behavior')),
);
break;
case 'remove':
behaviors.forEach((key) => {
self.extensions[mode] = self.extensions[mode].filter(
(behavior) => behavior.getKey() === key,
);
});
break;
case 'update':
behaviors.forEach((config) => {
if (isObject(config) && config.hasOwnProperty('key')) {
const behaviorItem = self.extensions[mode].find(behavior => behavior.getKey() === config.key);
if (behaviorItem) behaviorItem.updateConfig(config);
}
});
break;
default:
break;
modes.forEach(mode => {
// Do nothing if it's not the current mode.
// Changes are recorded in `graph.specification`. They are applied in onModeChange.
if (mode !== this.mode) {
return;
}
if (action === 'add') {
behaviors.forEach(config => {
const type = typeof config === 'string' ? config : (config as any).type;
const behavior = this.initBehavior(config);
if (behavior) {
this.addListeners(type, behavior);
}
});
return;
}
if (action === 'remove') {
behaviors.forEach(config => {
const type = typeof config === 'string' ? config : (config as any).type;
const behavior = this.behaviorMap.get(type);
if (behavior) {
this.removeListeners(type);
this.destroyBehavior(type, behavior);
}
});
return;
}
if (action === 'update') {
behaviors.forEach(config => {
const type = typeof config === 'string' ? config : (config as any).type;
const behavior = this.behaviorMap.get(type);
if (behavior) {
behavior.updateConfig(config);
}
});
}
});
}
private initEvents = () => {
Object.values(CANVAS_EVENT_TYPE).forEach(eventName => {
console.debug('Listen on canvas: ', eventName);
this.graph.canvas.document.addEventListener(eventName, this.handleCanvasEvent);
});
Object.values(DOM_EVENT_TYPE).forEach(eventName => {
this.graph.canvas.getContextService().getDomElement().addEventListener(eventName, this.handleDOMEvent);
});
}
private handleCanvasEvent = (gEvent: Event) => {
const debug = gEvent.type.includes('over') || gEvent.type.includes('move') ? () => {} : console.debug;
// Find the Node/Edge/Combo group element.
// const itemGroup = findItemGroup(gEvent.target as IElement);
// const itemType = itemGroup?.getAttribute('data-item-type') || 'canvas';
// const itemId = itemGroup?.getAttribute('data-item-id') || 'CANVAS';
const itemInfo = getItemInfoFromElement(gEvent.target as IElement);
if (!itemInfo) {
// This event was triggered from an element which belongs to none of the nodes/edges/canvas.
return;
}
// GEvent => G6Event
const { itemType, itemId } = itemInfo;
const event: IG6GraphEvent = {
...gEvent,
itemType,
itemId,
gEvent,
// Set currentTarget to this.graph instead of canvas.
// Because we add listeners like this: `graph.on('node:click', listener)`.
currentTarget: this.graph,
};
// Trigger a dblclick event.
if (event.type === 'click' && (event as any as MouseEvent).detail === 2) {
this.handleCanvasEvent({
...gEvent,
type: 'dblclick',
});
}
// Canvas Events
if (event.itemType === 'canvas') {
if (event.type === 'pointermove') {
this.handlePointerMove(event);
}
this.graph.emit(`canvas:${gEvent.type}`, event);
debug(`Canvas ${event.type} :`, event);
return;
}
// Item Events (Node/Edge)
if (itemType === 'node' || itemType === 'edge') {
if (event.type === 'pointermove' || event.type === 'pointerleave') {
this.handlePointerMove(event);
}
this.graph.emit(`${itemType}:${gEvent.type}`, event);
debug(`Item ${event.type} :`, event);
}
}
/**
* Emit item's pointerleave/pointerenter events when pointer moves on the canvas.
*/
private handlePointerMove = (event: IG6GraphEvent) => {
const prevItemInfo = this.prevItemInfo;
const curItemInfo = getItemInfoFromElement(event.target as IElement);
if (prevItemInfo?.itemId !== curItemInfo?.itemId) {
if (prevItemInfo) {
const preType = prevItemInfo.itemType;
this.graph.emit(`${preType}:pointerleave`, {
...event,
type: 'pointerleave',
target: prevItemInfo.groupElement,
});
console.debug(`${preType}:pointerleave`, {
...event,
type: 'pointerleave',
target: prevItemInfo.groupElement,
});
}
if (curItemInfo) {
const curType = curItemInfo.itemType;
this.graph.emit(`${curType}:pointerenter`, {
...event,
type: 'pointerenter',
target: curItemInfo.groupElement,
});
console.debug(`${curType}:pointerenter`, {
...event,
type: 'pointerenter',
target: curItemInfo.groupElement,
});
}
}
this.prevItemInfo = curItemInfo;
};
private handleDOMEvent = (event: Event) => {
this.graph.emit(event.type, event);
};
}

View File

@ -1,6 +1,6 @@
import { GraphChange, ID } from '@antv/graphlib';
import { ComboModel, IGraph } from '../../types';
import { registry } from '../../stdlib';
import registry from '../../stdlib';
import { getExtension } from '../../util/extension';
import { GraphCore } from '../../types/data';
import { NodeDisplayModel, NodeEncode, NodeModel, NodeModelData } from '../../types/node';
@ -81,7 +81,7 @@ export class ItemController {
private getExtensions() {
// TODO: user need to config using node/edge/combo types from useLib to spec?
const { node, edge, combo } = this.graph.getSpecification();
const nodeTypes = ['circle-node', 'custom-node']; // TODO: WIP
const edgeTypes = ['line-edge', 'custom-edge']; // TODO: WIP
const comboTypes = ['circle-combo', 'rect-combo']; // TODO: WIP
@ -248,7 +248,7 @@ export class ItemController {
/**
* The listener for item state changing.
* @param param
* @param param
* {
* ids: ids of the items to be set state
* states: state names to set
@ -327,7 +327,7 @@ export class ItemController {
* @param itemType item's type
* @param state state name
* @param value state value, true by default
* @returns
* @returns
*/
public findIdByState(itemType: ITEM_TYPE, state: string, value: string | boolean = true) {
const ids = [];

View File

@ -92,7 +92,7 @@ export default class Graph<B extends BehaviorRegistry> extends EventEmitter impl
}
this.backgroundCanvas = createCanvas(rendererType, container, width, height, pixelRatio);
this.canvas = createCanvas(rendererType, container, width, height, pixelRatio);
this.transientCanvas = createCanvas(rendererType, container, width, height, pixelRatio);
this.transientCanvas = createCanvas(rendererType, container, width, height, pixelRatio, true, { pointerEvents: 'none' });
Promise.all(
[this.backgroundCanvas, this.canvas, this.transientCanvas].map((canvas) => canvas.ready),
).then(() => (this.canvasReady = true));

View File

@ -0,0 +1,166 @@
import { Behavior } from '../../types/behavior';
import { IG6GraphEvent } from '../../types/event';
const ALLOWED_TRIGGERS = ['shift', 'ctrl', 'alt', 'meta'] as const;
type Trigger = (typeof ALLOWED_TRIGGERS)[number];
interface ClickSelectOptions {
/**
* Whether to allow multiple selection.
* Defaults to true.
* If set to false, `trigger` options will be ignored.
*/
multiple: boolean;
/**
* The key to pressed with mouse click to apply multiple selection.
* Defaults to `"shift"`.
* Could be "shift", "ctrl", "alt", or "meta".
*/
trigger: Trigger;
/**
* Item types to be able to select.
* Defaults to `["nodes"]`.
* Should be an array of "node", "edge", or "combo".
*/
itemTypes: Array<'node' | 'edge' | 'combo'>;
/**
* The state to be applied when select.
* Defaults to `"selected"`.
* Can be set to "active", "highlighted", etc.
*/
selectedState: 'selected'; // TODO: Enum
/**
* The event name to trigger when select/unselect an item.
*/
eventName: string;
/**
* Whether allow the behavior happen on the current item.
*/
shouldBegin: (event: IG6GraphEvent) => boolean;
/**
* Whether to update item state.
* If it returns false, you may probably listen to `eventName` and
* manage states or data manually
*/
shouldUpdate: (event: IG6GraphEvent) => boolean;
}
const DEFAULT_OPTIONS: ClickSelectOptions ={
multiple: true,
trigger: 'shift',
selectedState: 'selected',
itemTypes: ['node'],
eventName: '',
shouldBegin: () => true,
shouldUpdate: () => true,
};
export class ClickSelect extends Behavior {
options: ClickSelectOptions;
constructor(options: Partial<ClickSelectOptions>) {
super(Object.assign({}, DEFAULT_OPTIONS, options));
// Validate options
if (options.trigger && !ALLOWED_TRIGGERS.includes(options.trigger)) {
console.warn(`G6: Invalid trigger option "${options.trigger}" for click-select behavior!`);
this.options.trigger = DEFAULT_OPTIONS.trigger;
}
}
getEvents = () => {
return {
'node:click': this.onClick,
'canvas:click': this.onCanvasClick,
}
}
private isMultipleSelect = (event: MouseEvent) => {
if (!this.options.multiple) return false;
const key = this.options.trigger;
const keyMap: Record<Trigger, boolean> = {
shift: event.shiftKey,
ctrl: event.ctrlKey,
alt: event.altKey,
meta: event.metaKey,
};
return keyMap[this.options.trigger];
};
onClick = (event: IG6GraphEvent) => {
if (!this.options.shouldBegin(event)) return;
// Will not be 'canvas' because this method is listened on node and edge click.
const itemType = event.itemType as 'node' | 'edge' | 'combo';
const itemId = event.itemId;
const selectable = this.options.itemTypes.includes(itemType);
if (!selectable) return;
const state = this.options.selectedState;
const multiple = this.isMultipleSelect(event as any);
// FIXME: should use graph.getItemState() instead
const isSelectAction = this.graph.findIdByState(itemType, state).includes(itemId);
const action: 'select' | 'unselect' = isSelectAction ? 'unselect' : 'select';
// Select/Unselect item.
if (this.options.shouldUpdate(event)) {
if (multiple) {
this.graph.setItemState(itemId, state, isSelectAction);
} else {
// Not multiple, clear all currently selected items
const selectedItemIds = [
...this.graph.findIdByState('node', state),
...this.graph.findIdByState('edge', state),
...this.graph.findIdByState('combo', state),
];
this.graph.setItemState(selectedItemIds, state, false);
this.graph.setItemState(itemId, state, isSelectAction);
}
}
// Emit an event.
if (this.options.eventName) {
const selectedNodeIds = this.graph.findIdByState('node', state);
const selectedEdgeIds = this.graph.findIdByState('edge', state);
const selectedComboIds = this.graph.findIdByState('combo', state);
this.graph.emit(this.options.eventName, {
action,
itemId,
itemType,
selectedNodeIds,
selectedEdgeIds,
selectedComboIds,
});
}
};
onCanvasClick = (event: IG6GraphEvent) => {
if (!this.options.shouldBegin(event)) return;
// Find current selected items.
const state = this.options.selectedState;
const selectedItemIds = [
...this.graph.findIdByState('node', state),
...this.graph.findIdByState('edge', state),
...this.graph.findIdByState('combo', state),
];
if (!selectedItemIds.length) return;
// Unselect all items.
if (this.options.shouldUpdate(event)) {
this.graph.setItemState(selectedItemIds, state, false);
}
// Emit an event.
if (this.options.eventName) {
this.graph.emit(this.options.eventName, {
action: 'unselect',
itemId: '',
itemType: '',
selectedNodeIds: [],
selectedEdgeIds: [],
selectedComboIds: [],
});
}
}
};

View File

@ -1,22 +1,15 @@
import { uniqueId } from '@antv/util';
import { Behavior, BehaviorOption, BehaviorSpecification } from '../../types/behavior';
// TODO: definition of drag-canvas behavior
import { Behavior } from '../../types/behavior';
/**
* TODO: implement drag-canvas behavior
*/
interface DragCanvasOptions { key?: string, assistKey?: 'ctrl' | 'shift' };
export default class DragCanvas extends Behavior {
protected key: string;
protected options: DragCanvasOptions;
constructor(options: DragCanvasOptions) {
super(options);
}
public getKey: () => string = () => {
return this.key;
getEvents() {
return {};
}
public getSpec: () => BehaviorSpecification = () => {
return {} as BehaviorSpecification
};
public getEvents() {
return {}
}
public destroy() { }
}

View File

@ -1,6 +1,7 @@
import { registry as layoutRegistry } from '@antv/layout';
import { Lib } from '../types/stdlib';
import DragCanvas from './behavior/drag-canvas';
import { ClickSelect } from "./behavior/click-select";
import { comboFromNode } from './data/comboFromNode';
import { LineEdge } from './item/edge';
import { CircleNode } from './item/node';
@ -12,7 +13,8 @@ const stdLib = {
themes: {},
layouts: layoutRegistry,
behaviors: {
'drag-canvas': DragCanvas
'drag-canvas': DragCanvas,
'click-select': ClickSelect,
},
plugins: {},
nodes: {
@ -35,6 +37,6 @@ const useLib: Lib = {
combos: {},
};
const registry = { useLib };
export default registry;
export { stdLib, registry };
const registery = { useLib };
export default registery;
export { stdLib, registery };

View File

@ -1,31 +1,30 @@
import { uniqueId } from "@antv/util";
import { IG6GraphEvent } from "./event";
import { IGraph } from "./graph";
export interface BehaviorOption {
key?: string;
}
/**
* Base behavior.
* Two implementing ways: getSpec or getEvents
* TODO: Support spec mode.
*/
export abstract class Behavior {
protected key: string;
protected options: BehaviorOption = {};
constructor(options: BehaviorOption) {
this.key = options.key || uniqueId();
graph: IGraph;
options: any;
constructor(options: any) {
this.options = options;
};
public getKey = () => this.key;
abstract getSpec(): BehaviorSpecification;
abstract getEvents(): {
[eventName: string]: string
[eventName: string]: (event: IG6GraphEvent) => void;
};
public updateConfig = (options: BehaviorOption) => {
updateConfig = (options: any) => {
this.options = Object.assign(this.options, options);
}
abstract destroy(): void;
destroy() {};
}
/** Behavior regisry table.
/** Behavior registry table.
* @example { 'drag-node': DragNodeBehavior, 'my-drag-node': MyDragNodeBehavior }
*/
export interface BehaviorRegistry {
@ -47,4 +46,4 @@ export type BehaviorObjectOptionsOf<B extends BehaviorRegistry = {}> = {
/**
* TODO: interaction specification
*/
export interface BehaviorSpecification { }
export interface BehaviorSpecification { }

View File

@ -0,0 +1,45 @@
import { ID } from "@antv/graphlib";
import { IGraph } from "./graph";
/** Event type enum */
export enum CANVAS_EVENT_TYPE {
// Pointer events
'pointerover' = 'pointerover',
'pointerenter' = 'pointerenter',
'pointerdown' = 'pointerdown',
'pointermove' = 'pointermove',
'pointerup' = 'pointerup',
'pointerleave' = 'pointerleave',
// Mouse events
'click' = 'click',
'wheel' = 'wheel',
// 'dragstart',
// 'drag',
// 'dragend',
// 'dragenter',
// 'dragleave',
// 'dragover',
// 'dragout',
// 'drop',
}
export enum DOM_EVENT_TYPE {
'focus' = 'focus',
'blur' = 'blur',
'keyup' = 'keyup',
'keydown' = 'keydown',
'keypress' = 'keypress',
}
/** Event type union */
export type ICanvasEventType = `${CANVAS_EVENT_TYPE}`;
export interface IG6GraphEvent extends Omit<Event, 'currentTarget'> {
currentTarget: IGraph;
itemType: 'node' | 'edge' | 'combo' | 'canvas';
itemId: ID;
/** Original event emitted by G */
gEvent: Event;
};

View File

@ -6,13 +6,13 @@ import { isString } from '@antv/util';
/**
* Create a canvas
* @param { 'canvas' | 'svg' | 'webgl' } rendererType
* @param {string | HTMLElement} container
* @param { 'canvas' | 'svg' | 'webgl' } rendererType
* @param {string | HTMLElement} container
* @param {number} width
* @param {number} height
* @param {number} pixelRatio optional
* @param {boolean} customCanvasTag whether create a <canvas /> for multiple canvas under the container
* @returns
* @returns
*/
export const createCanvas = (
rendererType: 'canvas' | 'svg' | 'webgl',
@ -20,7 +20,8 @@ export const createCanvas = (
width: number,
height: number,
pixelRatio?: number,
customCanvasTag: boolean = true
customCanvasTag: boolean = true,
style: any = {},
) => {
let Renderer;
switch (rendererType.toLowerCase()) {
@ -42,6 +43,7 @@ export const createCanvas = (
canvasTag.style.width = `${width}px`;
canvasTag.style.height = `${height}px`;
canvasTag.style.position = 'fixed';
Object.assign(canvasTag.style, style);
const containerDOM = isString(container) ? document.getElementById('container') : container;
containerDOM!.appendChild(canvasTag);
return new Canvas({

View File

@ -0,0 +1,34 @@
import { IElement } from "@antv/g";
import { ID } from "@antv/graphlib";
export type ItemInfo = {
itemType: 'canvas' | 'node' | 'edge' | 'combo';
itemId: ID;
groupElement: IElement;
};
/**
* Given an element, which might be a child shape of a node/edge,
* get its belonging item.
*/
export const getItemInfoFromElement = (element: IElement): ItemInfo | null => {
if (element.nodeName === 'document') {
return {
itemType: 'canvas',
itemId: 'CANVAS',
groupElement: element,
};
}
let parent = element;
while (parent && !parent.getAttribute('data-item-type')) {
parent = element.parentElement;
}
if (!parent) return null;
return {
itemType: parent.getAttribute('data-item-type'),
itemId: parent.getAttribute('data-item-id'),
groupElement: parent,
};
}

View File

@ -0,0 +1,30 @@
import G6 from '../../src/index';
import { Behavior } from '../../src/types/behavior';
import { extend } from '../../src/util/extend';
const container = document.createElement('div');
document.querySelector('body').appendChild(container);
describe('click-select', () => {
it('x', () => {
const graph = new G6.Graph({
container,
width: 500,
height: 500,
type: 'graph',
data: {
nodes: [
{ id: 'node1', data: { x: 100, y: 200, keyShape: { fill: "#0f0" } } },
{ id: 'node2', data: { x: 200, y: 250, keyShape: { fill: "#f00" } } }
],
edges: [
{ id: 'edge1', source: 'node1', target: 'node2', data: { keyShape: { stroke: '#00f', lineWidth: 5 } } }
]
},
modes: {
default: [ 'click-select' ],
},
});
expect(true).toBe(true);
});
});