前言:今天看到有小伙伴看到antv/x6 多层嵌套下自动拓展父节点这篇文章后在底下留言怎么实现框组的功能,现在就单一摘出来讲解一下。
export class NodeGroup extends Node {
private collapsed: boolean = true;
protected postprocess() {
this.toggleCollapse(true);
}
isCollapsed() {
return this.collapsed;
}
nodeBBox(node: Cell) {
const padding = 40;
let rec: Rectangle;
let bboxs = node.children?.map((cell) => cell.getBBox());
if (bboxs == null) {
rec = new Rectangle(
(node as any).getPosition().x,
(node as any).getPosition().y,
200,
300
);
} else {
let x0 = Math.min(...bboxs.map((bbox) => bbox.left)) - padding / 2;
let y0 = Math.min(...bboxs.map((bbox) => bbox.top)) - padding;
let x1 =
Math.max(...bboxs.map((bbox) => bbox.right)) - x0 + padding / 2;
let y1 =
Math.max(...bboxs.map((bbox) => bbox.bottom)) - y0 + padding;
rec = new Rectangle(x0, y0, x1, y1);
}
return rec;
}
childHide(arr: Cell[], target: boolean) {
arr.forEach((child: cell) => {
target ? child.hide() : child.show();
});
}
parentResize(node: any) {
node.resize(this.nodeBBox(node).width, this.nodeBBox(node).height);
if (node.getParent()) {
this.parentResize(node.getParent());
}
}
toggleCollapse(collapsed?: boolean) {
const target = collapsed == null ? !this.collapsed : collapsed;
if (target) {
this.attr("buttonSign", {
d: "M0 2v10a2 2 90 002 2h14a2 2 90 002-2V4a2 2 90 00-2-2h-6l-2-2H2a2 2 90 00-2 2z",
});
this.resize(120, 40);
if (this.getParent()) {
// @ts-ignore
this.getParent().resize(
this.nodeBBox(this.getParent()!).width,
this.nodeBBox(this.getParent()!).height
);
}
} else {
this.attr("buttonSign", {
d: "M2 14a2 2 0 01-2-2V2a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M2 14h14a2 2 0 002-2v-5a2 2 0 00-2-2H6a2 2 0 00-2 2v5a2 2 0 01-2 2z",
});
this.resize(this.nodeBBox(this).width, this.nodeBBox(this).height);
if (this.getParent()) {
this.parentResize(this.getParent());
}
}
if (this.children) {
this.childHide(this.children, target);
}
this.collapsed = target;
}
}
NodeGroup.config({
shape: "rect",
markup: [
{
tagName: "rect",
selector: "body",
},
{
tagName: "image",
selector: "image",
},
{
tagName: "text",
selector: "text",
},
{
tagName: "g",
selector: "buttonGroup",
children: [
{
tagName: "rect",
selector: "button",
attrs: {
"pointer-events": "visiblePainted",
},
},
{
tagName: "path",
selector: "buttonSign",
attrs: {
fill: "none",
"pointer-events": "none",
},
},
],
},
],
attrs: {
body: {
rx: 5,
ry: 5,
refWidth: "100%",
refHeight: "100%",
strokeWidth: 1,
fill: "rgba(209,210,219,0.10)",
stroke: "#d1d2db",
},
text: {
fontSize: 12,
fill: "#fff",
refX: 2,
refY: 10,
ellipsis: true, // 文本超出显示范围时,自动添加省略号
},
buttonGroup: {
refX: "100%",
refX2: -25,
refY: 13,
},
button: {
height: 14,
width: 18,
rx: 2,
ry: 2,
fill: "rgba(245,245,245,0)",
stroke: "#ccc00",
cursor: "pointer",
event: "node:collapse",
},
buttonSign: {
stroke: "#d0d0d0",
},
},
});
Graph.registerNode("groupNode", NodeGroup);
上述代码定义了一个名为 NodeGroup 的类,它扩展了 Node 类,表示一个组节点。该类具有以下方法和属性:
collapsed 属性: 表示节点是否折叠,默认值为 true。
postprocess() 方法:在节点后处理时调用的方法,该方法调用 toggleCollapse(true) 方法来初始折叠节点。
isCollapsed() 方法:返回节点是否折叠的布尔值。
nodeBBox(node: Cell) 方法:计算节点的边界框(bounding box),并返回一个 Rectangle 对象。
childHide(arr: any[], target: boolean) 方法:根据指定的目标值隐藏或显示数组中的子元素。
parentResize(node: any) 方法:根据子节点的边界框调整父节点的大小。
toggleCollapse(collapsed?: boolean) 方法:切换节点的折叠状态。可选参数 collapsed 用于指定目标折叠状态。
config() 方法:配置节点的外观和属性。
在代码的最后,通过调用 Graph.registerNode(‘groupNode’, NodeGroup) 方法将 NodeGroup 类注册为 groupNode 的节点类型,以便在图形中使用。
const group = () => {
const cells = graph.getSelectedCells();
if (!cells || cells.length == 0) {
return;
}
graph.batchUpdate(() => {
let bboxs = cells.map((cell) => cell.getBBox());
let x0 = Math.min(...bboxs.map((bbox) => bbox.left)) - 40 / 2;
let y0 = Math.min(...bboxs.map((bbox) => bbox.top)) - 40;
let x1 = Math.max(...bboxs.map((bbox) => bbox.right)) - x0 + 20;
let y1 = Math.max(...bboxs.map((bbox) => bbox.bottom)) - y0 + 40;
let group: any = graph.addNode({
position: {
x: x0,
y: y0,
},
shape: "groupNode",
attrs: {
text: {
textWrap: {
text: "Group Name",
isFunNode: false,
type: "COMPOSED_ACTIVITY",
},
},
},
data: {
..., // 用户自定义数据
parent: true,
},
});
cells
.filter((cell: Cell) => cells.indexOf(cell.getParent()) == -1)
.forEach((cell) => {
group.addChild(cell);
});
(group as NodeGroup).toggleCollapse(false);
group.resize(x1, y1);
group.toBack({ deep: true });
});
};
上述代码定义了一个 group 函数,用于创建一个组节点。函数的主要逻辑如下:
获取当前选中的 cells,如果没有选中任何cell,则直接返回。
使用 graph.batchUpdate 方法批量更新图形,在更新过程中禁用自动刷新。
计算选中单元格的边界框 bboxs,并根据边界框计算组节点的位置和大小。
使用 graph.addNode 方法创建一个组节点 group,指定其位置、形状为 “groupNode”,并设置相应的属性和数据。
遍历选中的cells,将非父级节点的单元格添加为组节点的子节点。
调用 (group as NodeGroup).toggleCollapse(false) 方法将组节点展开,并将折叠状态应用于子节点。
调整组节点的大小为计算得出的大小。
将组节点置于最底层,使其在图形中处于底部。
结束批量更新,自动刷新图形。
const unGroup = () => {
const cells = graph.getSelectedCells();
cells.forEach((cell) => {
if (cell.shape != "groupNode") {
return;
} else {
graph.batchUpdate(() => {
(cell as NodeGroup).toggleCollapse(false);
cell.children?.forEach((child) => {
cell.unembed(child);
cell.parent?.embed(child);
});
cell.remove();
});
}
});
};
上述代码定义了一个unGroup 函数,用于解组选中的组节点。
函数的主要逻辑如下:
获取当前选中的cells。
对于每个cell,判断其形状是否为 “groupNode”。如果不是组节点,则直接返回,不进行解组操作。
如果是组节点,则进行解组操作:
使用 graph.batchUpdate 方法批量更新图形,在更新过程中禁用自动刷新。
调用 (cell as NodeGroup).toggleCollapse(false) 方法将组节点折叠状态设置为 false,即展开状态。
遍历组节点的子节点,使用 cell.unembed(child) 方法将子节点从组节点中移除。
使用 cell.parent?.embed(child) 方法将子节点重新嵌入其原父级节点中。
使用 cell.remove() 方法移除组节点。
完成解组操作后,结束批量更新,自动刷新图形。