@antv/g6
Version:
A Graph Visualization Framework in JavaScript
277 lines • 10.1 kB
JavaScript
import { Rect } from '@antv/g';
import { deepMix, isFunction } from '@antv/util';
import { CanvasEvent, CommonEvent } from '../constants';
import { idOf } from '../utils/id';
import { getBoundingPoints, isPointInPolygon } from '../utils/point';
import { Shortcut } from '../utils/shortcut';
import { BaseBehavior } from './base-behavior';
/**
* <zh/> 框选一组元素
*
* <en/> Brush select elements
*/
export class BrushSelect extends BaseBehavior {
constructor(context, options) {
super(context, deepMix({}, BrushSelect.defaultOptions, options));
this.shortcut = new Shortcut(context.graph);
this.onPointerDown = this.onPointerDown.bind(this);
this.onPointerMove = this.onPointerMove.bind(this);
this.onPointerUp = this.onPointerUp.bind(this);
this.clearStates = this.clearStates.bind(this);
this.bindEvents();
}
/**
* Triggered when the pointer is pressed
* @param event - Pointer event
* @internal
*/
onPointerDown(event) {
if (!this.validate(event) || !this.isKeydown() || this.startPoint)
return;
const { canvas, graph } = this.context;
const style = Object.assign({}, this.options.style);
// 根据缩放比例调整 lineWidth
// Adjust lineWidth according to the zoom ratio
if (this.options.style.lineWidth) {
style.lineWidth = +this.options.style.lineWidth / graph.getZoom();
}
this.rectShape = new Rect({ id: 'g6-brush-select', style });
canvas.appendChild(this.rectShape);
this.startPoint = [event.canvas.x, event.canvas.y];
}
/**
* Triggered when the pointer is moved
* @param event - Pointer event
* @internal
*/
onPointerMove(event) {
var _a;
if (!this.startPoint)
return;
const { immediately, mode } = this.options;
this.endPoint = getCursorPoint(event);
(_a = this.rectShape) === null || _a === void 0 ? void 0 : _a.attr({
x: Math.min(this.endPoint[0], this.startPoint[0]),
y: Math.min(this.endPoint[1], this.startPoint[1]),
width: Math.abs(this.endPoint[0] - this.startPoint[0]),
height: Math.abs(this.endPoint[1] - this.startPoint[1]),
});
if (immediately && mode === 'default')
this.updateElementsStates(getBoundingPoints(this.startPoint, this.endPoint));
}
/**
* Triggered when the pointer is released
* @param event - Pointer event
* @internal
*/
onPointerUp(event) {
if (!this.startPoint)
return;
if (!this.endPoint) {
this.clearBrush();
return;
}
this.endPoint = getCursorPoint(event);
this.updateElementsStates(getBoundingPoints(this.startPoint, this.endPoint));
this.clearBrush();
}
/**
* <zh/> 清除状态
*
* <en/> Clear state
* @internal
*/
clearStates() {
if (this.endPoint)
return;
this.clearElementsStates();
}
/**
* <zh/> 清除画布上所有元素的状态
*
* <en/> Clear the state of all elements on the canvas
* @internal
*/
clearElementsStates() {
const { graph } = this.context;
const states = Object.values(graph.getData()).reduce((acc, data) => {
return Object.assign({}, acc, data.reduce((acc, datum) => {
var _a;
const restStates = (_a = (datum.states || [])) === null || _a === void 0 ? void 0 : _a.filter((state) => state !== this.options.state);
acc[idOf(datum)] = restStates;
return acc;
}, {}));
}, {});
graph.setElementState(states, this.options.animation);
}
/**
* <zh/> 更新选中的元素状态
*
* <en/> Update the state of the selected elements
* @param points - <zh/> 框选区域的顶点 | <en/> The vertex of the selection area
* @internal
*/
updateElementsStates(points) {
const { graph } = this.context;
const { enableElements, state, mode, onSelect } = this.options;
const selectedIds = this.selector(graph, points, enableElements);
let states = {};
switch (mode) {
case 'union':
selectedIds.forEach((id) => {
states[id] = [...graph.getElementState(id), state];
});
break;
case 'diff':
selectedIds.forEach((id) => {
const prevStates = graph.getElementState(id);
states[id] = prevStates.includes(state) ? prevStates.filter((s) => s !== state) : [...prevStates, state];
});
break;
case 'intersect':
selectedIds.forEach((id) => {
const prevStates = graph.getElementState(id);
states[id] = prevStates.includes(state) ? [state] : [];
});
break;
case 'default':
default:
selectedIds.forEach((id) => {
states[id] = [state];
});
break;
}
if (isFunction(onSelect))
states = onSelect(states);
graph.setElementState(states, this.options.animation);
}
/**
* <zh/> 查找画布上在指定区域内显示的元素。当节点的包围盒中心在矩形内时,节点被选中;当边的两端节点在矩形内时,边被选中;当 combo 的包围盒中心在矩形内时,combo 被选中。
*
* <en/> Find the elements displayed in the specified area on the canvas. A node is selected if the center of its bbox is inside the rect; An edge is selected if both end nodes are inside the rect ;A combo is selected if the center of its bbox is inside the rect.
* @param graph - <zh/> 图实例 | <en/> Graph instance
* @param points - <zh/> 框选区域的顶点 | <en/> The vertex of the selection area
* @param itemTypes - <zh/> 元素类型 | <en/> Element type
* @returns <zh/> 选中的元素 ID 数组 | <en/> Selected element ID array
* @internal
*/
selector(graph, points, itemTypes) {
if (!itemTypes || itemTypes.length === 0)
return [];
const elements = [];
const graphData = graph.getData();
itemTypes.forEach((itemType) => {
graphData[`${itemType}s`].forEach((datum) => {
const id = idOf(datum);
if (graph.getElementVisibility(id) !== 'hidden' && isPointInPolygon(graph.getElementPosition(id), points)) {
elements.push(id);
}
});
});
// 如果边的两端节点都在框选范围内,则边也被选中 | If source node and target node are within the selection range, that edge is also selected
if (itemTypes.includes('edge')) {
const edges = graphData.edges;
edges === null || edges === void 0 ? void 0 : edges.forEach((edge) => {
const { source, target } = edge;
if (elements.includes(source) && elements.includes(target)) {
elements.push(idOf(edge));
}
});
}
return elements;
}
clearBrush() {
var _a;
(_a = this.rectShape) === null || _a === void 0 ? void 0 : _a.remove();
this.rectShape = undefined;
this.startPoint = undefined;
this.endPoint = undefined;
}
/**
* <zh/> 当前按键是否和 trigger 配置一致
*
* <en/> Is the current key consistent with the trigger configuration
* @returns <zh/> 是否一致 | <en/> Is consistent
* @internal
*/
isKeydown() {
const { trigger } = this.options;
const keys = (Array.isArray(trigger) ? trigger : [trigger]);
return this.shortcut.match(keys.filter((key) => key !== 'drag'));
}
/**
* <zh/> 验证是否启用框选
*
* <en/> Verify whether brush select is enabled
* @param event - <zh/> 事件 | <en/> Event
* @returns <zh/> 是否启用 | <en/> Whether to enable
* @internal
*/
validate(event) {
if (this.destroyed)
return false;
const { enable } = this.options;
if (isFunction(enable))
return enable(event);
return !!enable;
}
bindEvents() {
const { graph } = this.context;
graph.on(CommonEvent.POINTER_DOWN, this.onPointerDown);
graph.on(CommonEvent.POINTER_MOVE, this.onPointerMove);
graph.on(CommonEvent.POINTER_UP, this.onPointerUp);
graph.on(CanvasEvent.CLICK, this.clearStates);
}
unbindEvents() {
const { graph } = this.context;
graph.off(CommonEvent.POINTER_DOWN, this.onPointerDown);
graph.off(CommonEvent.POINTER_MOVE, this.onPointerMove);
graph.off(CommonEvent.POINTER_UP, this.onPointerUp);
graph.off(CanvasEvent.CLICK, this.clearStates);
}
/**
* <zh/> 更新配置项
*
* <en/> Update configuration
* @param options - <zh/> 配置项 | <en/> Options
* @internal
*/
update(options) {
this.unbindEvents();
this.options = deepMix(this.options, options);
this.bindEvents();
}
/**
* <zh/> 销毁
*
* <en/> Destroy
* @internal
*/
destroy() {
this.unbindEvents();
super.destroy();
}
}
BrushSelect.defaultOptions = {
animation: false,
enable: true,
enableElements: ['node', 'combo', 'edge'],
immediately: false,
mode: 'default',
state: 'selected',
trigger: ['shift'],
style: {
width: 0,
height: 0,
lineWidth: 1,
fill: '#1677FF',
stroke: '#1677FF',
fillOpacity: 0.1,
zIndex: 2,
pointerEvents: 'none',
},
};
export const getCursorPoint = (event) => {
return [event.canvas.x, event.canvas.y];
};
//# sourceMappingURL=brush-select.js.map