bpmn-js
Version:
A bpmn 2.0 toolkit and web modeler
425 lines (345 loc) • 12.5 kB
JavaScript
import {
add as collectionAdd,
remove as collectionRemove
} from 'diagram-js/lib/util/Collections';
import {
getEnclosedElements
} from 'diagram-js/lib/util/Elements';
import {
isLabel
} from '../../../util/LabelUtil';
import {
getBusinessObject,
is
} from '../../../util/ModelUtil';
import {
getOrCreateCategoryValueForGroup
} from '../behavior/util/CategoryUtil';
/**
* @typedef {import('diagram-js/lib/command/CommandHandler').default} CommandHandler
* @typedef {import('diagram-js/lib/core/ElementRegistry').default} ElementRegistry
* @typedef {import('../BpmnFactory').default} BpmnFactory
*
* @typedef {import('../../../model/Types').Element} Element
* @typedef {import('../../../model/Types').ModdleElement} ModdleElement
*
* @typedef { {
* categoryValue: ModdleElement;
* root?: Element;
* } } CategoryGroupEntry
*/
var CATEGORY_VALUE_REFS_ATTR = 'categoryValueRef';
/**
* Updates category value references for flow elements visually enclosed by groups.
*
* A flow element is grouped only when it shares the group's diagram root and
* is geometrically enclosed by the group. Consequently, visible children of an
* expanded subprocess are grouped, while children of a collapsed subprocess
* are not.
*
* Groups without a category value are auto-healed: if such a group visually
* encloses an affected flow element a category value is created for it on the
* fly, so that legacy diagrams (groups modeled without a category value) start
* to categorize their contents as soon as they are interacted with.
*
* @implements {CommandHandler}
*
* @param {BpmnFactory} bpmnFactory
* @param {ElementRegistry} elementRegistry
*/
export default function UpdateCategoryValueRefsHandler(bpmnFactory, elementRegistry) {
this._bpmnFactory = bpmnFactory;
this._elementRegistry = elementRegistry;
}
UpdateCategoryValueRefsHandler.$inject = [
'bpmnFactory',
'elementRegistry'
];
/**
* @param { Element[] } affectedFlowElements
* @param { {
* categoryValue: ModdleElement;
* groupShape: Element;
* }[] } affectedGroupEntries
*
* @return { {
* flowElements: {
* businessObject: ModdleElement;
* categoryValuesToAdd: ModdleElement[];
* categoryValuesToRemove: ModdleElement[];
* }[];
* groups: {
* groupShape: Element;
* categoryValue: ModdleElement;
* category: ModdleElement;
* }[];
* } }
*/
UpdateCategoryValueRefsHandler.prototype._computeUpdates = function(affectedFlowElements, affectedGroupEntries) {
var elementRegistry = this._elementRegistry,
bpmnFactory = this._bpmnFactory;
/** @type {Map<Element, ModdleElement>} */
var flowElementBusinessObjects = new Map();
var groupUpdates = [];
/**
* @type {Map<Element, {
* present?: CategoryGroupEntry;
* past?: CategoryGroupEntry;
* }>}
*/
var categoryGroups = new Map();
/**
* Adds a group to the list of groups to consider for category value updates.
*
* @param {Element} groupShape
* @param {ModdleElement} categoryValue
* @param {boolean} isPresent
*/
function addGroup(groupShape, categoryValue, isPresent) {
if (!categoryValue) {
return;
}
var categoryGroup = categoryGroups.get(groupShape);
if (!categoryGroup) {
categoryGroup = {};
categoryGroups.set(groupShape, categoryGroup);
}
if (isPresent && categoryGroup.present) {
return;
}
if (!isPresent && categoryGroup.past) {
return;
}
if (isPresent) {
categoryGroup.present = {
categoryValue: categoryValue,
root: getRoot(groupShape)
};
} else {
categoryGroup.past = {
categoryValue: categoryValue
};
}
}
/**
* Adds a flow element to the list of flow elements to consider for category
* value updates.
*
* @param {Element} flowElement
*/
function addFlowElement(flowElement) {
flowElementBusinessObjects.set(
flowElement,
getBusinessObject(flowElement)
);
}
/**
* Adds all flow elements on the diagram to the list of flow elements to
* consider; skips root elements (e.g. sub process planes) which share a
* business object with their collapsed shape but carry no diagram position.
*/
function addAllFlowElements() {
elementRegistry.filter(function(element) {
return is(element, 'bpmn:FlowElement') && !isLabel(element) && element.parent;
}).forEach(addFlowElement);
}
/**
* Returns a group's category value, creating one on the fly for
* legacy groups modeled without one. Retrieving the value in order to
* reference it materializes it; the fresh value is recorded as a heal so
* CategoryRootElementReferenceBehavior can maintain its root element.
*
* @param {Element} groupShape
*
* @return {ModdleElement}
*/
function getCategoryValue(groupShape) {
var groupBo = getBusinessObject(groupShape),
categoryValue = groupBo.get('categoryValueRef');
if (!categoryValue) {
categoryValue = getOrCreateCategoryValueForGroup(bpmnFactory, groupBo);
groupUpdates.push(createGroupUpdate(groupShape, categoryValue));
}
return categoryValue;
}
function getElementsByRoot(elements) {
return elements.reduce(function(elementsByRoot, element) {
var root = getRoot(element),
elementsInRoot = elementsByRoot.get(root);
if (!elementsInRoot) {
elementsInRoot = [];
elementsByRoot.set(root, elementsInRoot);
}
elementsInRoot.push(element);
return elementsByRoot;
}, new Map());
}
affectedFlowElements = Array.from(affectedFlowElements || []);
var affectedFlowElementsByRoot = getElementsByRoot(affectedFlowElements);
// when a group is moved, resized, added or removed, the category value
// references of all flow elements must be rechecked
if (affectedGroupEntries.length) {
addAllFlowElements();
}
// flow elements that were genuinely moved or created; unrelated groups
// will not be touched
affectedFlowElements.forEach(addFlowElement);
// add all present groups to the list of groups to consider for category
// value updates, auto-healing legacy groups only when an affected flow
// element moves into them
elementRegistry.filter(function(element) {
return is(element, 'bpmn:Group') && !isLabel(element) && element.parent;
}).forEach(function(groupShape) {
var categoryValue = getBusinessObject(groupShape).get('categoryValueRef');
if (!categoryValue) {
var groupRoot = getRoot(groupShape),
affectedFlowElementsInRoot = affectedFlowElementsByRoot.get(groupRoot) || [],
enclosedAffectedFlowElements = getEnclosedElements(
affectedFlowElementsInRoot,
groupShape
),
enclosesAffectedFlowElement = Object.keys(enclosedAffectedFlowElements).length > 0;
if (enclosesAffectedFlowElement) {
categoryValue = getCategoryValue(groupShape);
}
}
addGroup(groupShape, categoryValue, true);
});
affectedGroupEntries.forEach(function(groupEntry) {
addGroup(groupEntry.groupShape, groupEntry.categoryValue, false);
});
// a healed group receives a fresh category value; recheck all flow elements
// so that everything it visually encloses is tagged consistently, not just
// the flow element that triggered the healing
if (groupUpdates.length) {
addAllFlowElements();
}
var flowElements = Array.from(flowElementBusinessObjects.keys());
var managedCategoryValues = Array.from(categoryGroups.values()).reduce(function(set, group) {
if (group.present) {
set.add(group.present.categoryValue);
}
if (group.past) {
set.add(group.past.categoryValue);
}
return set;
}, new Set());
var categoryValuesByFlowElement = new Map(),
flowElementsByRoot = getElementsByRoot(flowElements);
// Containment is calculated once per group and diagram root. This avoids
// allocating an enclosure result for every flow element / group pair.
categoryGroups.forEach(function(categoryGroup, groupShape) {
var presentGroup = categoryGroup.present;
if (!presentGroup) {
return;
}
var flowElementsInRoot = flowElementsByRoot.get(presentGroup.root) || [],
enclosedFlowElements = getEnclosedElements(flowElementsInRoot, groupShape);
flowElementsInRoot.forEach(function(flowElement) {
if (!enclosedFlowElements[flowElement.id]) {
return;
}
var categoryValues = categoryValuesByFlowElement.get(flowElement);
if (!categoryValues) {
categoryValues = new Set();
categoryValuesByFlowElement.set(flowElement, categoryValues);
}
categoryValues.add(presentGroup.categoryValue);
});
});
// compute updates for all flow elements based on the groups they are inside
var flowElementUpdates = flowElements.reduce(function(flowElementUpdates, flowElement) {
var flowElementBo = flowElementBusinessObjects.get(flowElement),
categoryValueRefs = flowElementBo.get(CATEGORY_VALUE_REFS_ATTR),
categoryValueRefSet = new Set(categoryValueRefs),
enclosingCategoryValues = categoryValuesByFlowElement.get(flowElement) || new Set(),
categoryValuesToAdd = Array.from(enclosingCategoryValues).filter(function(categoryValue) {
return !categoryValueRefSet.has(categoryValue);
}),
categoryValuesToRemove = categoryValueRefs.filter(function(categoryValue) {
return managedCategoryValues.has(categoryValue) &&
!enclosingCategoryValues.has(categoryValue);
});
if (categoryValuesToAdd.length || categoryValuesToRemove.length) {
flowElementUpdates.push({
businessObject: flowElementBo,
categoryValuesToAdd,
categoryValuesToRemove
});
}
return flowElementUpdates;
}, []);
return {
flowElements: flowElementUpdates,
groups: groupUpdates
};
};
UpdateCategoryValueRefsHandler.prototype.execute = function(context) {
var updates = context.updates;
if (!updates) {
updates = context.updates = this._computeUpdates(
context.affectedFlowElements,
context.affectedGroupEntries
);
}
// set auto-healed category values first so that flow element updates can
// reference them; root element linking is handled by
// CategoryRootElementReferenceBehavior
applyGroupUpdates(updates.groups);
applyFlowElementUpdates(updates.flowElements);
return [];
};
UpdateCategoryValueRefsHandler.prototype.revert = function(context) {
var updates = context.updates;
revertFlowElementUpdates(updates.flowElements);
revertGroupUpdates(updates.groups);
return [];
};
function createGroupUpdate(groupShape, categoryValue) {
return {
groupShape: groupShape,
categoryValue: categoryValue,
category: categoryValue.$parent
};
}
function applyGroupUpdates(groupUpdates) {
groupUpdates.forEach(function(groupUpdate) {
getBusinessObject(groupUpdate.groupShape).categoryValueRef = groupUpdate.categoryValue;
groupUpdate.categoryValue.$parent = groupUpdate.category;
});
}
function revertGroupUpdates(groupUpdates) {
groupUpdates.forEach(function(groupUpdate) {
getBusinessObject(groupUpdate.groupShape).categoryValueRef = null;
});
}
function applyFlowElementUpdates(flowElementUpdates) {
flowElementUpdates.forEach(function(flowElementUpdate) {
var businessObject = flowElementUpdate.businessObject,
categoryValueRefs = businessObject.get(CATEGORY_VALUE_REFS_ATTR);
flowElementUpdate.categoryValuesToRemove.forEach(function(categoryValue) {
collectionRemove(categoryValueRefs, categoryValue);
});
flowElementUpdate.categoryValuesToAdd.forEach(function(categoryValue) {
collectionAdd(categoryValueRefs, categoryValue, true);
});
});
}
function revertFlowElementUpdates(flowElementUpdates) {
flowElementUpdates.forEach(function(flowElementUpdate) {
var businessObject = flowElementUpdate.businessObject,
categoryValueRefs = businessObject.get(CATEGORY_VALUE_REFS_ATTR);
flowElementUpdate.categoryValuesToAdd.forEach(function(categoryValue) {
collectionRemove(categoryValueRefs, categoryValue);
});
flowElementUpdate.categoryValuesToRemove.forEach(function(categoryValue) {
collectionAdd(categoryValueRefs, categoryValue, true);
});
});
}
function getRoot(element) {
while (element.parent) {
element = element.parent;
}
return element;
}