@logicflow/extension
Version:
LogicFlow Extensions
543 lines (509 loc) • 18.7 kB
text/typescript
/**
* @deprecated
* 待废弃,2.0 版本将 提供 dynamic-group 支持分组功能,当前 Group 插件设计和实现有比较多的问题,后续不再维护,请及时切换
*/
import { forEach, isEmpty, isObject } from 'lodash-es'
import LogicFlow, {
BaseEdgeModel,
BaseNodeModel,
EventType,
Model,
} from '@logicflow/core'
import GroupNode, { GroupNodeModel } from './GroupNode'
import GraphConfigData = LogicFlow.GraphConfigData
import EdgeConfig = LogicFlow.EdgeConfig
import NodeData = LogicFlow.NodeData
import Point = LogicFlow.Point
import BoxBoundsPoint = Model.BoxBoundsPoint
import NodeConfig = LogicFlow.NodeConfig
const DEFAULT_TOP_Z_INDEX = -1000
const DEFAULT_BOTTOM_Z_INDEX = -10000
export class Group {
static pluginName = 'group'
private lf: LogicFlow
topGroupZIndex = DEFAULT_BOTTOM_Z_INDEX
activeGroup: any
nodeGroupMap: Map<string, string> = new Map()
constructor({ lf }: LogicFlow.IExtensionProps) {
lf.register(GroupNode)
this.lf = lf
lf.graphModel.addNodeMoveRules((model, deltaX, deltaY) => {
if (model.isGroup) {
// 如果移动的是分组,那么分组的子节点也跟着移动。
const nodeIds = this.getNodeAllChild(model)
lf.graphModel.moveNodes(nodeIds, deltaX, deltaY, true)
return true
}
const groupModel = lf.getNodeModelById(
this.nodeGroupMap.get(model.id)!,
) as GroupNodeModel
if (groupModel && groupModel.isRestrict) {
// 如果移动的节点存在分组中,且这个分组禁止子节点移出去。
const { minX, minY, maxX, maxY } = model.getBounds()
return groupModel.isAllowMoveTo({
minX: minX + deltaX,
minY: minY + deltaY,
maxX: maxX + deltaX,
maxY: maxY + deltaY,
})
}
return true
})
lf.graphModel.group = this
lf.on('node:add,node:drop,node:dnd-add', this.appendNodeToGroup)
lf.on('node:delete', this.deleteGroupChild)
lf.on('node:dnd-drag,node:drag', this.setActiveGroup)
lf.on('node:click', this.nodeSelected)
lf.on('graph:rendered', this.graphRendered)
// https://github.com/didi/LogicFlow/issues/1346
// 重写 addElements() 方法,在 addElements() 原有基础上增加对 group 内部所有 nodes 和 edges 的复制功能
lf.addElements = (
{ nodes: selectedNodes, edges: selectedEdges }: GraphConfigData,
distance: number,
): {
nodes: BaseNodeModel[]
edges: BaseEdgeModel[]
} => {
// ============== 变量初始化 ==============
const nodeIdMap: Record<string, string> = {}
const elements: any = {
nodes: [],
edges: [],
}
const groupInnerEdges: EdgeConfig[] = []
// ============== 变量初始化 ==============
forEach(selectedNodes, (node) => {
const preId = node.id
const { children, ...rest } = node
const nodeModel = lf.addNode(rest)
if (!nodeModel) {
return {
nodes: [],
edges: [],
}
}
if (preId) nodeIdMap[preId] = nodeModel.id
elements.nodes.push(nodeModel) // group的nodeModel
// 递归创建group的nodeModel的children
const { edgesArray } = this.createAllChildNodes(
nodeIdMap,
children as Set<string>,
nodeModel,
distance,
)
groupInnerEdges.push(...edgesArray)
})
groupInnerEdges.forEach((edge) => {
this.createEdgeModel(edge, nodeIdMap, distance)
})
// 构建的时候直接偏移,这里不需要再进行再度偏移
// groupInnerChildren.nodes.forEach(node => this.translateNodeData(node, distance));
// groupInnerChildren.edges.forEach(edge => this. translateEdgeData(edge, distance));
// 最外层的edges继续执行创建edgeModel的流程
// 由于最外层会调用 translateEdgeData(),因此这里不用传入distance进行偏移
forEach(selectedEdges, (edge) => {
const edgeModel = this.createEdgeModel(edge, nodeIdMap, 0)
elements.edges.push(edgeModel)
})
// 返回elements进行选中效果,即触发element.selectElementById()
// shortcut.ts也会对最外层的nodes和edges进行偏移,即translateNodeData()
return elements
}
}
/**
* 创建一个Group类型节点内部的所有子节点的副本
* 并且在遍历所有nodes的过程中顺便拿到所有edges(只在Group范围的edges)
*/
createAllChildNodes(
nodeIdMap: Record<string, string>,
children: Set<string>,
current: BaseNodeModel,
distance: number,
) {
const { lf } = this
const edgesDataArray: EdgeConfig[] = []
const edgesNodeModelArray: BaseEdgeModel[] = []
const nodesArray: BaseNodeModel[] = []
children?.forEach((childId: string) => {
const childNodeModel = lf.getNodeModelById(childId)
if (childNodeModel) {
const {
x,
y,
properties,
type,
rotate,
children,
// incoming,
// outgoing,
} = childNodeModel
const nodeConfig: NodeConfig = {
x: x + distance,
y: y + distance,
properties,
type,
rotate,
// 如果不传递type,会自动触发NODE_ADD
// 有概率触发appendToGroup
}
const eventType =
EventType.NODE_GROUP_COPY || ('node:group-copy-add' as EventType)
const newChildModel = lf.addNode(nodeConfig, eventType)
;(current as GroupNodeModel).addChild(newChildModel.id)
nodeIdMap[childId] = newChildModel.id
nodesArray.push(newChildModel)
// 存储children内部节点相关的输入边
childNodeModel.incoming.edges.forEach((edge) => {
edgesNodeModelArray.push(edge)
})
// 存储children内部节点相关的输出边
childNodeModel.outgoing.edges.forEach((edge) => {
edgesNodeModelArray.push(edge)
})
if (children instanceof Set) {
const { nodesArray: childNodes, edgesArray: childEdges } =
this.createAllChildNodes(
nodeIdMap,
children,
newChildModel,
distance,
)
nodesArray.push(...childNodes)
edgesDataArray.push(...childEdges)
}
}
})
// 1. 判断每一条边的开始节点和目标节点是否在Group中
const filterEdgesArray: BaseEdgeModel[] = edgesNodeModelArray.filter(
(edge: BaseEdgeModel) =>
nodeIdMap[edge.sourceNodeId] && nodeIdMap[edge.targetNodeId],
)
// 2. 为每一条group的内部边构建出EdgeData数据
// 从GraphModel.ts的getSelectElements()可以知道EdgeConfig就是EdgeData
const filterEdgesDataArray: EdgeConfig[] = filterEdgesArray.map((item) =>
item.getData(),
)
return {
nodesArray,
edgesArray: edgesDataArray.concat(filterEdgesDataArray), // ??? what's this
}
}
createEdgeModel(
edge: EdgeConfig,
nodeIdMap: Record<string, string>,
distance: number,
) {
const { lf } = this
let sourceId = edge.sourceNodeId
let targetId = edge.targetNodeId
if (nodeIdMap[sourceId]) sourceId = nodeIdMap[sourceId]
if (nodeIdMap[targetId]) targetId = nodeIdMap[targetId]
const { type, startPoint, endPoint, pointsList, text } = edge
// ====== 仿造shortcut.ts的 translateEdgeData()逻辑 ======
const newStartPoint = {
x: (startPoint?.x || 0) + distance,
y: (startPoint?.y || 0) + distance,
}
const newEndPoint = {
x: (endPoint?.x || 0) + distance,
y: (endPoint?.y || 0) + distance,
}
let newPointsList: Point[] = []
if (pointsList && pointsList.length > 0) {
newPointsList = pointsList.map((point) => {
point.x += distance
point.y += distance
return point
})
}
const edgeConfig: EdgeConfig = {
type,
startPoint: newStartPoint,
endPoint: newEndPoint,
sourceNodeId: sourceId,
targetNodeId: targetId,
pointsList: newPointsList,
}
if (isObject(text) && !isEmpty(text)) {
edgeConfig.text = {
...text,
x: text?.x + distance,
y: text?.y + distance,
}
}
// ====== 仿造shortcut.ts的 translateEdgeData()逻辑 ======
// 简化复制时的参数传入,防止创建出两个edge属于同个group这种情况
return lf.graphModel.addEdge(edgeConfig)
}
/**
* 获取一个节点内部所有的子节点,包裹分组的子节点
*/
getNodeAllChild(model: GroupNodeModel | BaseNodeModel) {
let nodeIds: string[] = []
if (model.children) {
;(model as GroupNodeModel).children.forEach((nodeId) => {
nodeIds.push(nodeId)
const nodeModel = this.lf.getNodeModelById(nodeId)
if (nodeModel?.isGroup) {
nodeIds = nodeIds.concat(this.getNodeAllChild(nodeModel))
}
})
}
return nodeIds
}
graphRendered = ({ data }: { data: LogicFlow.GraphData }) => {
// 如果节点
if (data && data.nodes) {
data.nodes.forEach((node) => {
if (node.children) {
;(node.children as string[]).forEach((nodeId) => {
this.nodeGroupMap.set(nodeId, node.id)
})
}
})
// 初始化nodes时进行this.topGroupZIndex的校准更新
this.checkAndCorrectTopGroupZIndex(data.nodes)
}
}
appendNodeToGroup = ({ data }: { data: NodeData }) => {
// 如果这个节点之前已经在group中了,则将其从之前的group中移除
const preGroupId = this.nodeGroupMap.get(data.id)
if (preGroupId) {
const preGroup = this.lf.getNodeModelById(preGroupId) as GroupNodeModel
preGroup.removeChild(data.id)
this.nodeGroupMap.delete(data.id)
preGroup.setAllowAppendChild(false)
}
// 然后再判断这个节点是否在某个group中,如果在,则将其添加到对应的group中
const nodeModel = this.lf.getNodeModelById(data.id)
const bounds = nodeModel?.getBounds()
if (bounds && nodeModel) {
const group = this.getGroup(bounds, data) as GroupNodeModel
// https://github.com/didi/LogicFlow/issues/1261
// 当使用SelectionSelect框选后触发lf.addNode(Group)
// 会触发appendNodeToGroup()的执行
// 由于this.getGroup()会判断node.id !== nodeData.id
// 因此当addNode是Group类型时,this.getGroup()会一直返回空
// 导致了下面这段代码无法执行,也就是无法将当前添加的Group添加到this.nodeGroupMap中
// 这导致了折叠分组时触发的foldEdge()无法正确通过getNodeGroup()拿到正确的groupId
// 从而导致折叠分组时一直都会创建一个虚拟边
// 而初始化分组时由于正确设置了nodeGroupMap的数据,因此不会产生虚拟边的错误情况
if (nodeModel.isGroup) {
// 如果这个节点是分组,那么将其子节点也记录下来
;(data.children as Set<string>).forEach((nodeId) => {
this.nodeGroupMap.set(nodeId, data.id)
})
// 新增node时进行this.topGroupZIndex的校准更新
this.checkAndCorrectTopGroupZIndex([data])
this.nodeSelected({
data,
isSelected: false,
isMultiple: false,
})
}
if (!group) return
const isAllowAppendIn = group.isAllowAppendIn(data)
if (!isAllowAppendIn) {
this.lf.emit('group:not-allowed', {
group: group.getData(),
node: data,
})
return
}
group.addChild(data.id)
this.nodeGroupMap.set(data.id, group.id)
group.setAllowAppendChild(false)
}
}
deleteGroupChild = ({ data }: { data: NodeData }) => {
// 如果删除的是分组节点,则同时删除分组的子节点
if (data.children) {
;(data.children as Set<string>).forEach((nodeId) => {
this.nodeGroupMap.delete(nodeId)
this.lf.deleteNode(nodeId)
})
}
const groupId = this.nodeGroupMap.get(data.id)
if (groupId) {
const group = this.lf.getNodeModelById(groupId) as GroupNodeModel
group.removeChild(data.id)
this.nodeGroupMap.delete(data.id)
}
}
setActiveGroup = ({ data }: { data: NodeData }) => {
const nodeModel = this.lf.getNodeModelById(data.id)
const bounds = nodeModel?.getBounds()
if (nodeModel && bounds) {
const newGroup = this.getGroup(bounds, data)
if (this.activeGroup) {
this.activeGroup.setAllowAppendChild(false)
}
if (!newGroup || (nodeModel.isGroup && newGroup.id === data.id)) return
const isAllowAppendIn = (newGroup as GroupNodeModel).isAllowAppendIn(data)
if (!isAllowAppendIn) {
return
}
this.activeGroup = newGroup
this.activeGroup.setAllowAppendChild(true)
}
}
findNodeAndChildMaxZIndex = (nodeModel: BaseNodeModel) => {
let maxZIndex = DEFAULT_BOTTOM_Z_INDEX
if (nodeModel.isGroup) {
maxZIndex = Math.max(maxZIndex, nodeModel.zIndex)
}
if (nodeModel.children) {
;(nodeModel as GroupNodeModel).children.forEach((nodeId) => {
if (typeof nodeId === 'object') {
// 正常情况下, GroupNodeModel.children是一个id数组,这里只是做个兼容
// @ts-ignore
nodeId = nodeId.id
}
const child = this.lf.getNodeModelById(nodeId)
if (child?.isGroup) {
const childMaxZIndex = this.findNodeAndChildMaxZIndex(child)
maxZIndex = Math.max(childMaxZIndex, maxZIndex)
}
})
}
return maxZIndex
}
checkAndCorrectTopGroupZIndex = (nodes: NodeData[]) => {
// 初始化时/增加新节点时,找出新增nodes的最大zIndex
let maxZIndex = DEFAULT_BOTTOM_Z_INDEX
nodes.forEach((node: NodeData) => {
const nodeModel = this.lf.getNodeModelById(node.id)
if (nodeModel) {
const currentNodeMaxZIndex = this.findNodeAndChildMaxZIndex(nodeModel)
if (currentNodeMaxZIndex > maxZIndex) {
maxZIndex = currentNodeMaxZIndex
}
}
})
if (this.topGroupZIndex >= maxZIndex) {
// 一般是初始化时/增加新节点时发生,因为外部强行设置了一个很大的zIndex
// 删除节点不会影响目前最高zIndex的赋值
return
}
// 新增nodes中如果存在zIndex比this.topGroupZIndex大
// 说明this.topGroupZIndex已经失去意义,代表不了目前最高zIndex的group,需要重新校准
// https://github.com/didi/LogicFlow/issues/1535
// 当外部直接设置多个BaseNode.zIndex=1时
// 当点击某一个node时,由于这个this.topGroupZIndex是从-10000开始计算的,this.topGroupZIndex+1也就是-9999
// 这就造成当前点击的node的zIndex远远比其它node的zIndex小,因此造成zIndex错乱问题
const allGroupNodes = this.lf.graphModel.nodes.filter(
(node: BaseNodeModel) => node.isGroup,
)
let max = this.topGroupZIndex
for (let i = 0; i < allGroupNodes.length; i++) {
const groupNode = allGroupNodes[i]
if (groupNode.zIndex > max) {
max = groupNode.zIndex
}
}
this.topGroupZIndex = max
}
/**
* 1. 分组节点默认在普通节点下面。
* 2. 分组节点被选中后,会将分组节点以及其内部的其他分组节点放到其余分组节点的上面。
* 3. 分组节点取消选中后,不会将分组节点重置为原来的高度。
* 4. 由于LogicFlow核心目标是支持用户手动绘制流程图,所以不考虑一张流程图超过1000个分组节点的情况。
*/
nodeSelected = ({
data,
isMultiple,
isSelected,
}: {
data: NodeData
isMultiple: boolean
isSelected: boolean
}) => {
const nodeModel = this.lf.getNodeModelById(data.id)
this.toFrontGroup(nodeModel)
// 重置所有的group zIndex,防止group节点zIndex增长为正。
if (this.topGroupZIndex > DEFAULT_TOP_Z_INDEX) {
this.topGroupZIndex = DEFAULT_BOTTOM_Z_INDEX
const allGroups = this.lf.graphModel.nodes
.filter((node) => node.isGroup)
.sort((a, b) => a.zIndex - b.zIndex)
let preZIndex = 0
for (let i = 0; i < allGroups.length; i++) {
const group = allGroups[i]
if (group.zIndex !== preZIndex) {
this.topGroupZIndex++
preZIndex = group.zIndex
}
group.setZIndex(this.topGroupZIndex)
}
}
// FIX #1004
// 如果节点被多选,
// 这个节点是分组,则将分组的所有子节点取消选中
// 这个节点是分组的子节点,且其所属分组节点已选,则取消选中
if (isMultiple && isSelected) {
if (nodeModel?.isGroup) {
;(nodeModel as GroupNodeModel).children.forEach((child) => {
const childModel = this.lf.graphModel.getElement(child)
childModel?.setSelected(false)
})
} else {
const groupId = this.nodeGroupMap.get(data.id)
if (groupId) {
const groupModel = this.lf.getNodeModelById(groupId)
groupModel?.isSelected && nodeModel?.setSelected(false)
}
}
}
}
toFrontGroup = (model?: BaseNodeModel) => {
if (!model || !model.isGroup) {
return
}
this.topGroupZIndex++
model.setZIndex(this.topGroupZIndex)
if (model.children) {
;(model as GroupNodeModel).children.forEach((nodeId) => {
const node = this.lf.getNodeModelById(nodeId)
this.toFrontGroup(node)
})
}
}
/**
* 获取自定位置其所属分组
* 当分组重合时,优先返回最上层的分组
*/
getGroup(
bounds: BoxBoundsPoint,
nodeData: NodeData,
): BaseNodeModel | undefined {
const { nodes } = this.lf.graphModel
const groups = nodes.filter(
(node) =>
node.isGroup &&
(node as GroupNodeModel).isInRange(bounds) &&
node.id !== nodeData.id,
)
if (groups.length === 0) return
if (groups.length === 1) return groups[0]
let topGroup = groups[groups.length - 1]
for (let i = groups.length - 2; i >= 0; i--) {
if (groups[i].zIndex > topGroup.zIndex) {
topGroup = groups[i]
}
}
return topGroup
}
/**
* 获取某个节点所属的groupModel
*/
getNodeGroup(nodeId: string) {
const groupId = this.nodeGroupMap.get(nodeId)
if (groupId) {
return this.lf.getNodeModelById(groupId)
}
}
render() {}
destroy() {}
}
export * from './GroupNode'
export default GroupNode