import {assign, forEach, isArray} from 'min-dash' import {is} from 'bpmn-js/lib/util/ModelUtil' import {isAny} from 'bpmn-js/lib/features/modeling/util/ModelingUtil' import {changeElementSize} from '@/components/VueBpmn/custom/util' export default class CustomContextPad { constructor( config, contextPad, create, elementFactory, injector, translate, modeling, bpmnFactory, elementRegistry, eventBus ) { this.create = create this.elementFactory = elementFactory this.translate = translate this.modeling = modeling this.bpmnFactory = bpmnFactory this._model = bpmnFactory._model this.elementRegistry = elementRegistry this.eventBus = eventBus if (config.autoPlace !== false) { this.autoPlace = injector.get('autoPlace', false) } contextPad.registerProvider(this) // // 定义这是一个contextPad } getContextPadEntries(element) { var actions = {} if (element.type === 'label') { return actions } var elementBusinessObject = element.businessObject const { autoPlace, create, elementFactory, translate, modeling, bpmnFactory, elementRegistry, eventBus } = this // x左边 y坐标移动的间距 const moveDistance = 126 // 指线的方向 const directConst = {down2right: 'down2right', right2down: 'right2down'} // 分支条件节点 同步条件节点 条件条件节点 const gateWayArray = [ 'bpmn:ExclusiveGateway', 'bpmn:ParallelGateway', 'bpmn:InclusiveGateway' ] // 删除功能 function removeElement(e) { let moveUp = 0 let removeArr = [element] // if (element.type == 'bpmn:SequenceFlow') { // modeling.removeElements(removeArr) // return // } // 条件节点中某分支 删除最后一个元素时,连接线需要删掉 element.incoming.forEach(item => { removeArr.push(item) }) element.outgoing.forEach(item => { removeArr.push(item) }) // 大部分时候删除完 需要重新连线 let incomingEl = element.incoming[0].source let outgoingEl = element.outgoing[0].target // 如果删除的是 条件节点 需要成对删除 if (element.id.endsWith('end') || element.id.endsWith('start')) { let otherElId = element.id.endsWith('end') ? element.id.replace('end', 'start') : element.id.replace('start', 'end') // 收集两个元素中间所有元素并删除 removeArr.push(elementRegistry.get(otherElId)) let startEl = element.id.endsWith('start') ? element : elementRegistry.get(otherElId) let endEl = element.id.endsWith('end') ? element : elementRegistry.get(otherElId) collectGatewayEl(startEl, endEl.id, removeArr) incomingEl = startEl.incoming[0].source outgoingEl = endEl.outgoing[0].target } // 条件节点删除分支 只剩小于2条时,需把条件节点内东西都干掉 if (incomingEl.id.endsWith('start') && outgoingEl.id.endsWith('end')) { if (incomingEl.outgoing.length <= 2) { // 删除条件节点前后两条线 incomingEl.incoming.forEach(item => { removeArr.push(item) }) outgoingEl.outgoing.forEach(item => { removeArr.push(item) }) collectGatewayEl(incomingEl, outgoingEl.id, removeArr) incomingEl = incomingEl.incoming[0].source outgoingEl = outgoingEl.outgoing[0].target } } let incomingElOrigin = incomingEl.width / 2 + incomingEl.x let outgoingElOrigin = outgoingEl.width / 2 + outgoingEl.x let direct = '' if (incomingElOrigin > outgoingElOrigin) { direct = directConst.down2right } if (incomingElOrigin < outgoingElOrigin) { direct = directConst.right2down } // 删除元素 modeling.removeElements(removeArr) // 如果开始和结束 都是条件节点,不需要连线; if (!(incomingEl.id.endsWith('start') && outgoingEl.id.endsWith('end'))) { // 连线 customUpdateWaypoints(null, incomingEl, outgoingEl, direct) // 条件节点中移动元素特殊处理, 找到结束条件节点,查看连线中最短距离决定是否移动 if (outgoingEl.id.endsWith('end')) { let maxY = 0 outgoingEl.incoming.forEach(item=>{ if (item.waypoints[0].y > maxY) { maxY = item.waypoints[0].y } }) moveUp = outgoingEl.y - maxY - moveDistance } else if(incomingEl.id.endsWith('start')) { } else { moveUp = outgoingEl.y - incomingEl.y - moveDistance } // 删除完后一般需要移动元素 删除条件节点的某条分支时不移动 if (moveUp > 0) { moveElementUp(outgoingEl, -moveUp) } } } // function moveElementUp(startEl, moveDistance = -126) { // 获取元素和元素 let moveArr = [] collectGatewayEl(startEl, null, moveArr) modeling.moveElements(moveArr, {x: 0, y: moveDistance}) } function getMoveElement(el, moveArr) { moveArr.push(el) if (startEl.outgoing && startEl.outgoing.length) { startEl.outgoing.forEach(item => { collectGatewayEl(item.target, endId, arr) }) } } // 收集两个条件节点中间所有元素包括线 function collectGatewayEl(startEl, endId, arr) { arr.push(startEl) if (startEl.id == endId) { return } if (startEl.outgoing && startEl.outgoing.length) { startEl.outgoing.forEach(item => { collectGatewayEl(item.target, endId, arr) }) } } function clickElement(e) {} /** * Create an append action * * @param {string} type * @param {string} className * @param {string} [title] * @param {Object} [options] * * @return {Object} descriptor */ function appendAction(type, className, title, options) { if (typeof title !== 'string') { options = title title = translate('Append {type}', {type: type.replace(/^bpmn:/, '')}) } function appendStart(event, element) { var shape = elementFactory.createShape(assign({type: type}, options)) changeElementSize(shape) create.start(event, shape, { source: element }) } var append = autoPlace ? (event, element) => { // createBranchByShape(element); var shape = elementFactory.createShape( assign({type: type}, options) ) changeElementSize(shape) // 如果点击的是线 if (is(element.businessObject, 'bpmn:SequenceFlow')) { customMoveElements(element, shape) let source = element.source let position = { x: source.x + source.width / 2, y: source.y + moveDistance } let hints = null let connection = element let newShape = modeling.appendShape( source, shape, position, element.parent, hints ) modeling.reconnectStart( connection, newShape, {}, {connectionStart: {x: shape.x + shape.width / 2, y: shape.y}} ) // 如果是条件节点 需要成对出现 并在中间添加用户任务 } else if (gateWayArray.includes(type)) { var shapeEnd = elementFactory.createShape( assign( { type: type === 'bpmn:ExclusiveGateway' ? 'bpmn:UserTask' : type }, options ) ) shape.id += 'start' shapeEnd.id = shape.id.replace('start', 'end') // 第一条分支中用户任务 var userTaskShape1 = elementFactory.createShape({ type: 'bpmn:UserTask' }) // 第二条分支中用户任务 var userTaskShape2 = elementFactory.createShape({ type: 'bpmn:UserTask' }) changeElementSize(shapeEnd) changeElementSize(userTaskShape1) changeElementSize(userTaskShape2) // 新增条件节点和 第一条分支 midInsertOrAppend(element, shapeEnd) midInsertOrAppend(element, userTaskShape1) midInsertOrAppend(element, shape) // 新增第二条分支 let startEl = elementRegistry.get(shape.id) midInsertOrAppend(startEl, userTaskShape2) // appendGateway(element, shape); } else { midInsertOrAppend(element, shape) } } : appendStart return { group: 'model', className: className, title: title, action: { dragstart: appendStart, click: append } } } /** * shape 为条件节点时 自动创建两条分支 并且 element 的目标节点为结束节点 * @param {*} element * @param {*} shape */ function appendGateway(element, shape) { if ( !isAny(shape.businessObject, [ 'bpmn:ExclusiveGateway', 'bpmn:ParallelGateway', 'bpmn:InclusiveGateway' ]) ) return if (is(shape.businessObject, 'bpmn:ExclusiveGateway')) { appendGatewayXor(element, shape) } } /** * 默认接两个用户任务 * * @param {*} element * @param {*} shape */ function appendGatewayXor(element, shape) { // element. let userTaskShape1 = shape.outgoing[0].target if (!is(shape.outgoing[0].target.businessObject, 'bpmn:UserTask')) { userTaskShape1 = elementFactory.createShape({type: 'bpmn:UserTask'}) changeElementSize(userTaskShape1) // 下移并且添加第一条分支 moveElementsAndAppendShape(shape, userTaskShape1) } // 第一条分支继续下移 customMoveElements(shape, userTaskShape1, null, false) const userTaskShape2 = elementFactory.createShape({type: 'bpmn:UserTask'}) changeElementSize(userTaskShape2) midInsertOrAppend(shape, userTaskShape2) customUpdateWaypoints( userTaskShape2.incoming[0], shape, userTaskShape2, directConst.right2down ) customUpdateWaypoints( null, userTaskShape2, userTaskShape1, directConst.down2right ) } /** * 如果没有connection 则需要创建一条source 到target的线 * @param {*} connection * @param {*} source * @param {*} target * @param {*} direct */ function customUpdateWaypoints(connection, source, target, direct) { if (!connection) { connection = modeling.connect(source, target) } let updateWaypoints = [] switch (direct) { case directConst.down2right: // source 下中 连接 target 右中 updateWaypoints.push({x: source.x + source.width / 2, y: source.y}) updateWaypoints.push({ x: source.x + source.width / 2, y: target.y + target.height / 2 }) updateWaypoints.push({ x: target.x + target.width, y: target.y + target.height / 2 }) break case directConst.right2down: // source 右中 连接 target 上中 updateWaypoints.push({ x: source.x + source.width, y: source.y + source.height / 2 }) updateWaypoints.push({ x: target.x + target.width / 2, y: source.y + source.height / 2 }) updateWaypoints.push({x: target.x + target.width / 2, y: target.y}) break default: // 上到下 updateWaypoints.push({x: source.x + source.width / 2, y: source.y}) updateWaypoints.push({x: target.x + target.width / 2, y: target.y}) } // 更新锚点(巡航点) modeling.updateWaypoints(connection, updateWaypoints) modeling.layoutConnection(connection) } /** * 中间插入或者追加 */ function midInsertOrAppend(element, shape) { let position = { x: element.x + element.width / 2, y: element.y + moveDistance } // 追加 if ( ((gateWayArray.includes(element.type) && element.outgoing && element.outgoing.length >= 1) || element.outgoing.length == 0) && !element.id.endsWith('end') ) { let endEvent = elementRegistry.get(element.id.replace('start', 'end')) // let targetShape = element.outgoing[0].target.incoming; position = { x: element.x + element.width + moveDistance * element.outgoing.length, y: element.y + moveDistance + element.height / 2 } // 创建元素 modeling.createShape(shape, position, element.parent) // 连接元素 // modeling.connect(element, shape); // (8) 创建一条新的连线连接task shape到endEvent事件 // modeling.createConnection(shape, endEvent, { type: 'bpmn:SequenceFlow' }, element.parent); // 改变连接线方向 customUpdateWaypoints(null, element, shape, directConst.right2down) customUpdateWaypoints(null, shape, endEvent, directConst.down2right) // 需要连线 自动连接到第一条线的任务 if (element.outgoing.length >= 2) { // customUpdateWaypoints(targetShape,element,shape,directConst.right2down); // customUpdateWaypoints(null,shape,targetShape,directConst.down2right); } // 如果是分支条件节点 // if ("bpmn:ExclusiveGateway" == element.type && !gateWay.includes(shape.type)) { // modeling.connect(userTaskShape2, userTaskShape1); // let connection = userTaskShape2.outgoing[0]; // modeling.updateWaypoints(connection, [{ x: userTaskShape2.x + userTaskShape2.width / 2, y: userTaskShape2.y }, { x: userTaskShape2.x + userTaskShape2.width / 2, y: userTaskShape1.y + userTaskShape1.height / 2 }, { x: userTaskShape1.x + userTaskShape1.width, y: userTaskShape1.y + userTaskShape1.height / 2 }]) // } //modeling.updateWaypoints } else { moveElementsAndAppendShape(element, shape) // 判断新增的元素是否是处于两个条件节点中间 judgeInGateway(shape) } } function judgeInGateway(element) { let startGateway = null let endGateway = null findStartGateway(element) findEndGateway(element) // 找到是一对条件节点 if (startGateway && endGateway && startGateway.id.replace('_start','') == endGateway.id.replace('_end','')) { let maxY = 0 endGateway.incoming.forEach(item=>{ if (item.waypoints[0].y > maxY) { maxY = item.waypoints[0].y } }) let moveUp = endGateway.y - maxY - moveDistance if(moveUp > 0) { moveElementUp(endGateway, -moveUp) } } function findStartGateway(el) { if (el.id.endsWith('start')) { startGateway = el return } if (el.incoming && el.incoming.length) { findStartGateway(el.incoming[0].source) } } function findEndGateway(el) { if (el.id.endsWith('end')) { endGateway = el return } if (el.outgoing && el.outgoing.length) { el.outgoing.forEach(item=>{ findEndGateway(item.target) }) } } } /** * 自定义计算两个节点的连线 */ function customConnect(source, target) { modeling.connect(source, target) // userTaskShape2 ==> userTaskShape1 let connection = source.outgoing[0] modeling.updateWaypoints(connection, [ {x: source.x + source.width / 2, y: source.y}, {x: source.x + source.width / 2, y: target.y + target.height / 2}, {x: target.x + target.width, y: target.y + target.height / 2} ]) } /** * 元素下移 然后在目标节点新增指定节点 * @param {*} element * @param {*} shape */ function moveElementsAndAppendShape(element, shape, moveY = 80) { customMoveElements(element, shape, moveY) // 如果是线添加 if (is(element.businessObject, 'bpmn:SequenceFlow')) { // 画线 modeling.connect(element.source, shape) element.source.outgoing.remove(element) element.source = shape modeling.layoutConnection(element, { connectionStart: {x: shape.x + shape.width / 2, y: shape.y} }) } else { // 通过CustomAutoPlaceBehavior.js 计算位置 // autoPlace.append(element, shape); // modeling.createShape( // shape, // { // x: element.x + 0, // y: element.y + 50 // } // ); // modeling.connect(element, shape); let position = { x: element.x + element.width / 2, y: element.y + element.height / 2 + moveDistance } let hints = null let newShape = modeling.appendShape( element, shape, position, element.parent, hints ) // 主动触发事件 autoPlace.end 自动对齐结束事件 eventBus.fire('autoPlace.end', { source: element, shape: newShape }) // 元素连线 let connection = element.outgoing[0] // element.outgoing.remove(connection); // connection.source = shape; modeling.reconnectStart( connection, newShape, {}, {connectionStart: {x: newShape.x + newShape.width / 2, y: newShape.y}} ) modeling.layoutConnection(connection) // 自定义的id需主动更新一次 否则保存后,还是保存的旧ID,而且更新时id相同会报错,此处多加了下划线区分(框架问题) if (shape.id.endsWith('end') || shape.id.endsWith('start')) { let newId = shape.id.endsWith('end') ? shape.id.replace('end', '_end') : shape.id.replace('start', '_start') let newEl = elementRegistry.get(shape.id) modeling.updateProperties(newEl, {id: newId}) } } } /** * element outgoing的所有元素下移 * @param {*} element * @param {*} moveY */ function customMoveElements(element, shape, moveY = 80, includeYourself) { if (!moveY) { moveY = 80 } if (is(element.businessObject, 'bpmn:SequenceFlow')) { element = element.source } // 如果线长度大于46+ 80 不移动 if (shape.y - element.y > 125) { return } if (!element.outgoing || element.outgoing.length == 0) { return } let elements = [] collectBehindEelemnts(element, elements, includeYourself) let shapeHeight = 46 if (shape && shape.height) { shapeHeight = shape.height } modeling.moveElements(elements, {x: 0, y: moveY + shapeHeight}) } /** * 需要考虑自己连接自己导致死循环 * @param {} element * @param {*} elements * @returns */ function collectBehindEelemnts(element, elements = [], includeYourself) { element.outgoing.forEach(connection => { if (elements.indexOf(connection.target) == -1) { elements.push(connection.target) collectBehindEelemnts(connection.target, elements) } }) if (includeYourself) { elements.push(element) } return elements } function appendTask(event, element) { if (autoPlace) { const shape = elementFactory.createShape({type: 'bpmn:UserTask'}) changeElementSize(shape) midInsertOrAppend(element, shape) } else { appendTaskStart(event, element) } } function appendTaskStart(event, element) { const shape = elementFactory.createShape({type: 'bpmn:UserTask'}) changeElementSize(shape) create.start(event, shape, element) } function editElement() { // 创建编辑图标 return { group: 'edit', className: 'icon-custom icon-custom-edit', title: translate('编辑'), action: { click: clickElement } } } function deleteElement() { return { delete: { group: 'edit', className: 'bpmn-icon-trash', title: translate('删除'), action: { click: removeElement } } } } function appendAndClickAnnotation(color) { return function(event, element) { const businessObject = bpmnFactory.create('bpmn:TextAnnotation') if (color) { businessObject.color = color } const shape = elementFactory.createShape({ type: 'bpmn:TextAnnotation', businessObject }) autoPlace.append(element, shape) } } function createSignTask(append, isSequential) { return function(event, element) { let loopCharacteristics = bpmnFactory.create( 'bpmn:MultiInstanceLoopCharacteristics' ) const businessObject = bpmnFactory.create('bpmn:UserTask', { loopCharacteristics: loopCharacteristics }) businessObject['custom'] = 1 businessObject.name = '会签并行任务' if (isSequential) { loopCharacteristics.isSequential = true businessObject.name = '会签串行任务' } const shape = elementFactory.createShape({ type: 'bpmn:UserTask', businessObject }) changeElementSize(shape) if (append) { midInsertOrAppend(element, shape) } else { create.start(event, shape) } } } function createSubprocessCollapsed(append) { return function(event, element) { const businessObject = bpmnFactory.create('bpmn:CallActivity') businessObject['custom'] = 1 businessObject.name = '外部子流程' const shape = elementFactory.createShape({ type: 'bpmn:CallActivity', businessObject }) changeElementSize(shape) if (append) { autoPlace.append(element, shape) } else { create.start(event, shape) } } } // 自定义右键菜单 如果点击的是线 不出现自定义菜单 if (element.type != 'bpmn:SequenceFlow') { actions = { 'append.user-task': appendAction( 'bpmn:UserTask', 'bpmn-icon-user-task', translate('追加用户任务') ), 'append.sign-task': { group: 'model', className: 'bpmn-icon-parallel-mi-marker', title: translate('追加会签并行任务'), action: { click: createSignTask(true, false), dragstart: createSignTask(false, false) } }, 'append.sign-task-sequential': { group: 'model', className: 'bpmn-icon-sequential-mi-marker', title: translate('追加会签串行任务'), action: { click: createSignTask(true, true), dragstart: createSignTask(false, true) } } } } // 开始节点没有条件节点 点击线可以加条件节点 || is(elementBusinessObject, 'bpmn:SequenceFlow') // !isAny(elementBusinessObject, ['bpmn:StartEvent']) && if (!gateWayArray.includes(element.type)) { assign(actions, { 'append.gateway': appendAction( 'bpmn:ExclusiveGateway', 'bpmn-icon-gateway-xor', translate('追加分支条件节点') ), 'append.inclusiveGateway': appendAction( 'bpmn:InclusiveGateway', 'bpmn-icon-gateway-or', translate('追加条件同步条件节点') ), 'append.parallelGateway': appendAction( 'bpmn:ParallelGateway', 'bpmn-icon-gateway-parallel', translate('追加同步条件节点') ) }) } // 删除按钮 assign(actions, deleteElement()) return actions } } CustomContextPad.$inject = [ 'config', 'contextPad', 'create', 'elementFactory', 'injector', 'translate', 'modeling', 'bpmnFactory', 'elementRegistry', 'eventBus' ]