Stencil
在本章节中主要介绍 Stencil 插件相关的知识,通过阅读,你可以了解到
- 如何通过 Stencil 提升 Dnd 能力
- 如何配置分组、折叠与搜索
使用
Stencil 是在 Dnd 基础上的进一步封装,提供了一个类似侧边栏的 UI 组件,并支持分组、折叠、搜索等能力。你可以通过插件 Stencil 启用该侧边栏模板功能,示例:
import { Graph, Stencil } from '@antv/x6'
const graph = new Graph({
background: {
color: '#F2F7FA',
},
})
const stencil = new Stencil({
target: graph,
groups: [
{
name: 'group1',
},
{
name: 'group2',
},
],
})
const rect1 = graph.createNode({
shape: 'rect',
width: 100,
height: 40,
})
const rect2 = rect1.clone()
// 需要一个容纳 stencil 的 Dom 容器 stencilContainer
stencilContainer.appendChild(stencil.container)
stencil.load([rect1, rect2], 'group1')
演示
import './index.less'
import { createRoot } from 'react-dom/client'
import React from 'react'
import { Graph, Stencil, Snapline } from '@antv/x6'
const commonAttrs = {
body: {
fill: '#fff',
stroke: '#8f8f8f',
strokeWidth: 1,
},
}
export default class Example extends React.Component {
private container: HTMLDivElement
private stencilContainer: HTMLDivElement
componentDidMount() {
const graph = new Graph({
container: this.container,
background: {
color: '#F2F7FA',
},
})
graph.use(
new Snapline({
enabled: true,
sharp: true,
}),
)
const source = graph.addNode({
x: 130,
y: 30,
width: 100,
height: 40,
label: 'Hello',
attrs: {
body: {
stroke: '#8f8f8f',
strokeWidth: 1,
fill: '#fff',
rx: 6,
ry: 6,
},
},
})
const target = graph.addNode({
x: 320,
y: 240,
width: 100,
height: 40,
label: 'World',
attrs: {
body: {
stroke: '#8f8f8f',
strokeWidth: 1,
fill: '#fff',
rx: 6,
ry: 6,
},
},
})
graph.addEdge({
source,
target,
attrs: {
line: {
stroke: '#8f8f8f',
strokeWidth: 1,
},
},
})
graph.centerContent()
const stencil = new Stencil({
title: 'Stencil',
target: graph,
search(cell, keyword) {
return cell.shape.indexOf(keyword) !== -1
},
placeholder: 'Search by shape name',
notFoundText: 'Not Found',
collapsable: true,
stencilGraphHeight: 0,
groups: [
{
name: 'group1',
title: 'Group(Collapsable)',
},
{
name: 'group2',
title: 'Group',
collapsable: false,
},
],
})
this.stencilContainer.appendChild(stencil.container)
const n1 = graph.createNode({
shape: 'rect',
x: 40,
y: 40,
width: 80,
height: 40,
label: 'rect',
attrs: commonAttrs,
})
const n2 = graph.createNode({
shape: 'circle',
x: 180,
y: 40,
width: 40,
height: 40,
label: 'circle',
attrs: commonAttrs,
})
const n3 = graph.createNode({
shape: 'ellipse',
x: 280,
y: 40,
width: 80,
height: 40,
label: 'ellipse',
attrs: commonAttrs,
})
const n4 = graph.createNode({
shape: 'path',
x: 420,
y: 40,
width: 40,
height: 40,
// https://www.svgrepo.com/svg/13653/like
path: 'M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z',
attrs: commonAttrs,
label: 'path',
})
stencil.load([n1, n2], 'group1')
stencil.load([n3, n4], 'group2')
}
refContainer = (container: HTMLDivElement) => {
this.container = container
}
refStencil = (container: HTMLDivElement) => {
this.stencilContainer = container
}
render() {
return (
<div className="stencil-app">
<div className="app-stencil" ref={this.refStencil} />
<div className="app-content" ref={this.refContainer} />
</div>
)
}
}
const root = createRoot(document.getElementById('container')!)
root.render(<Example />)
window.addEventListener('pagehide', () => root.unmount(), { once: true })
.stencil-app {
display: flex;
border: 1px solid #f0f0f0;
padding: 16px 8px;
font-family: sans-serif;
.app-stencil {
position: relative;
width: 200px;
border: 1px solid #f0f0f0;
}
.app-content {
flex: 1;
height: 380px;
margin-right: 8px;
margin-left: 8px;
box-shadow: 0 0 10px 1px #e9e9e9;
}
.x6-widget-stencil-group-title {
// 避免 dumi 的样式影响
margin-top: 0 !important;
}
}配置
| 选项 | 类型 | 必选 | 默认值 | 说明 |
|---|---|---|---|---|
| title | string | 'Stencil' |
标题。 | |
| groups | Group[] | ✓️ | - | 分组信息。 |
| search | Filter | false |
搜索选项。 | |
| placeholder | string | 'Search' |
搜索文本框的 placeholder 文本。 | |
| notFoundText | string | 'No matches found' |
未匹配到搜索结果时的提示文本。 | |
| collapsable | boolean | false |
是否显示全局折叠/展开按钮。 | |
| layout | (this: Stencil, model: Model, group?: Group | null) => any | 网格布局 | 模板画布中节点的布局方法。 | |
| layoutOptions | any | - | 布局选项。 | |
| stencilGraphWidth | number | 200 |
模板画布宽度。 | |
| stencilGraphHeight | number | 800 |
模板画布高度。设置为 0 时高度会自适应。 | |
| stencilGraphPadding | number | 10 |
模板画布边距。 | |
| stencilGraphOptions | Graph.Options | - | 模板画布选项。 |
提示
除了上面的配置,Stencil 还继承了 Dnd 的所有配置。
分组
初始化时,按照 groups 提供的分组,在每个分组中会渲染一个模板画布。分组的类型定义如下:
export interface Group {
name: string // 分组名称
title?: string // 分组标题,缺省时使用 `name`
collapsable?: boolean // 分组是否可折叠,默认为 true
collapsed?: boolean // 初始状态是否为折叠状态
nodeMovable?: boolean // 模板画布中的节点能否拖动
graphWidth?: number // 模板画布宽度
graphHeight?: number // 模板画布高度
graphPadding?: number // 模板画布边距
graphOptions?: Graph.Options // 模板画布选项
layout?: (this: Stencil, model: Model, group?: Group | null) => any
layoutOptions?: any // 布局选项
}
可以看到分组内的一些配置和外层的配置有重合,比如 graphWidth/stencilGraphWidth 与 graphHeight/stencilGraphHeight,分组内的配置优先级更高。
布局
添加节点时,使用分组或全局的 layout 和 layoutOptions 来对节点进行自动布局,默认使用网格布局方法来布局模板节点,支持的布局选项有:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| columns | number | 2 |
网格布局的列数,默认为 2。行数根据节点数自动计算。 |
| columnWidth | number | ‘auto’ | ‘compact’ | 'auto' |
列宽。auto: 所有节点中最宽节点的宽度作为列宽,compact: 该列中最宽节点的宽度作为列宽。 |
| rowHeight | number | ‘auto’ | ‘compact’ | 'auto' |
行高。auto: 所有节点中最高节点的高度作为行高,compact: 该行中最高节点的高度作为行高。 |
| dx | number | 10 |
单元格在 X 轴的偏移量,默认为 10。 |
| dy | number | 10 |
单元格在 Y 轴的偏移量,默认为 10。 |
| marginX | number | 0 |
单元格在 X 轴的边距,默认为 0。 |
| marginY | number | 0 |
单元格在 Y 轴的边距,默认为 0。 |
| center | boolean | true |
节点是否与网格居中对齐,默认为 true。 |
| resizeToFit | boolean | false |
是否自动调整节点的大小来适应网格大小,默认为 false。 |
也可以按照 (this: Stencil, model: Model, group?: Group | null) => any 签名进行自定义布局。
搜索
Stencil 还提供了搜索能力。
// 只搜索 rect 节点
const stencil = new Stencil({
search: (cell, keyword, groupName, stencil) => {
if (keyword) {
return cell.shape === 'rect'
}
return true
},
})
// 搜索 text 包含关键字的 rect 节点
const stencil = new Stencil({
search: (cell, keyword, groupName, stencil) => {
if (keyword) {
return cell.shape === 'rect' && cell.attr('text/text').includes(keyword)
}
return true
},
})
API
stencil.load(…)
load(nodes: (Node | Node.Metadata)[], groupName?: string): this
加载节点。参数如下:
| 名称 | 类型 | 必选 | 默认值 | 描述 |
|---|---|---|---|---|
| nodes | (Node | Node.Metadata)[] |
✓️ | - | 加载的节点。 |
| groupName | string | - | 加载节点的分组名称。 |
stencil.unload(…)
unload(nodes: (Node | Node.Metadata)[], groupName?: string): this
卸载节点。参数如下:
| 名称 | 类型 | 必选 | 默认值 | 描述 |
|---|---|---|---|---|
| nodes | (Node | Node.Metadata)[] |
✓️ | - | 卸载的节点。 |
| groupName | string | - | 卸载节点的分组名称。 |
stencil.addGroup(…)
addGroup(group: Stencil.Group | Stencil.Group[])
新增分组。
stencil.addGroup({
name: 'group1',
graphHeight: 100,
})
stencil.removeGroup(…)
removeGroup(groupName: string | string[])
删除分组。
stencil.resizeGroup(…)
resizeGroup(groupName: string, size: { width: number; height: number })
修改分组的尺寸。