bpmn-js/lib/features/modeling/behavior/LabelBehavior.js

403 lines
8.8 KiB
JavaScript

import {
assign
} from 'min-dash';
import inherits from 'inherits';
import {
is,
getBusinessObject
} from '../../../util/ModelUtil';
import {
isLabelExternal,
getExternalLabelMid,
hasExternalLabel,
isLabel
} from '../../../util/LabelUtil';
import {
getLabel
} from '../../label-editing/LabelUtil';
import {
getLabelAdjustment
} from './util/LabelLayoutUtil';
import CommandInterceptor from 'diagram-js/lib/command/CommandInterceptor';
import {
getNewAttachPoint
} from 'diagram-js/lib/util/AttachUtil';
import {
getMid,
roundPoint
} from 'diagram-js/lib/layout/LayoutUtil';
import {
delta
} from 'diagram-js/lib/util/PositionUtil';
import {
sortBy
} from 'min-dash';
import {
getDistancePointLine,
perpendicularFoot
} from './util/GeometricUtil';
var DEFAULT_LABEL_DIMENSIONS = {
width: 90,
height: 20
};
var NAME_PROPERTY = 'name';
var TEXT_PROPERTY = 'text';
/**
* A component that makes sure that external labels are added
* together with respective elements and properly updated (DI wise)
* during move.
*
* @param {EventBus} eventBus
* @param {Modeling} modeling
* @param {BpmnFactory} bpmnFactory
* @param {TextRenderer} textRenderer
*/
export default function LabelBehavior(
eventBus, modeling, bpmnFactory,
textRenderer) {
CommandInterceptor.call(this, eventBus);
// update label if name property was updated
this.postExecute('element.updateProperties', function(e) {
var context = e.context,
element = context.element,
properties = context.properties;
if (NAME_PROPERTY in properties) {
modeling.updateLabel(element, properties[NAME_PROPERTY]);
}
if (TEXT_PROPERTY in properties
&& is(element, 'bpmn:TextAnnotation')) {
var newBounds = textRenderer.getTextAnnotationBounds(
{
x: element.x,
y: element.y,
width: element.width,
height: element.height
},
properties[TEXT_PROPERTY] || ''
);
modeling.updateLabel(element, properties.text, newBounds);
}
});
// create label shape after shape/connection was created
this.postExecute([ 'shape.create', 'connection.create' ], function(e) {
var context = e.context,
hints = context.hints || {};
if (hints.createElementsBehavior === false) {
return;
}
var element = context.shape || context.connection,
businessObject = element.businessObject;
if (isLabel(element) || !isLabelExternal(element)) {
return;
}
// only create label if attribute available
if (!getLabel(element)) {
return;
}
var labelCenter = getExternalLabelMid(element);
// we don't care about x and y
var labelDimensions = textRenderer.getExternalLabelBounds(
DEFAULT_LABEL_DIMENSIONS,
getLabel(element)
);
modeling.createLabel(element, labelCenter, {
id: businessObject.id + '_label',
businessObject: businessObject,
width: labelDimensions.width,
height: labelDimensions.height
});
});
// update label after label shape was deleted
this.postExecute('shape.delete', function(event) {
var context = event.context,
labelTarget = context.labelTarget,
hints = context.hints || {};
// check if label
if (labelTarget && hints.unsetLabel !== false) {
modeling.updateLabel(labelTarget, null, null, { removeShape: false });
}
});
// update di information on label creation
this.postExecute([ 'label.create' ], function(event) {
var context = event.context,
element = context.shape,
businessObject,
di;
// we want to trigger on real labels only
if (!element.labelTarget) {
return;
}
// we want to trigger on BPMN elements only
if (!is(element.labelTarget || element, 'bpmn:BaseElement')) {
return;
}
businessObject = element.businessObject,
di = businessObject.di;
if (!di.label) {
di.label = bpmnFactory.create('bpmndi:BPMNLabel', {
bounds: bpmnFactory.create('dc:Bounds')
});
}
assign(di.label.bounds, {
x: element.x,
y: element.y,
width: element.width,
height: element.height
});
});
function getVisibleLabelAdjustment(event) {
var context = event.context,
connection = context.connection,
label = connection.label,
hints = assign({}, context.hints),
newWaypoints = context.newWaypoints || connection.waypoints,
oldWaypoints = context.oldWaypoints;
if (typeof hints.startChanged === 'undefined') {
hints.startChanged = !!hints.connectionStart;
}
if (typeof hints.endChanged === 'undefined') {
hints.endChanged = !!hints.connectionEnd;
}
return getLabelAdjustment(label, newWaypoints, oldWaypoints, hints);
}
this.postExecute([
'connection.layout',
'connection.updateWaypoints'
], function(event) {
var context = event.context,
hints = context.hints || {};
if (hints.labelBehavior === false) {
return;
}
var connection = context.connection,
label = connection.label,
labelAdjustment;
// handle missing label as well as the case
// that the label parent does not exist (yet),
// because it is being pasted / created via multi element create
//
// Cf. https://github.com/bpmn-io/bpmn-js/pull/1227
if (!label || !label.parent) {
return;
}
labelAdjustment = getVisibleLabelAdjustment(event);
modeling.moveShape(label, labelAdjustment);
});
// keep label position on shape replace
this.postExecute([ 'shape.replace' ], function(event) {
var context = event.context,
newShape = context.newShape,
oldShape = context.oldShape;
var businessObject = getBusinessObject(newShape);
if (businessObject
&& isLabelExternal(businessObject)
&& oldShape.label
&& newShape.label) {
newShape.label.x = oldShape.label.x;
newShape.label.y = oldShape.label.y;
}
});
// move external label after resizing
this.postExecute('shape.resize', function(event) {
var context = event.context,
shape = context.shape,
newBounds = context.newBounds,
oldBounds = context.oldBounds;
if (hasExternalLabel(shape)) {
var label = shape.label,
labelMid = getMid(label),
edges = asEdges(oldBounds);
// get nearest border point to label as reference point
var referencePoint = getReferencePoint(labelMid, edges);
var delta = getReferencePointDelta(referencePoint, oldBounds, newBounds);
modeling.moveShape(label, delta);
}
});
}
inherits(LabelBehavior, CommandInterceptor);
LabelBehavior.$inject = [
'eventBus',
'modeling',
'bpmnFactory',
'textRenderer'
];
// helpers //////////////////////
/**
* Calculates a reference point delta relative to a new position
* of a certain element's bounds
*
* @param {Point} point
* @param {Bounds} oldBounds
* @param {Bounds} newBounds
*
* @return {Delta} delta
*/
export function getReferencePointDelta(referencePoint, oldBounds, newBounds) {
var newReferencePoint = getNewAttachPoint(referencePoint, oldBounds, newBounds);
return roundPoint(delta(newReferencePoint, referencePoint));
}
/**
* Generates the nearest point (reference point) for a given point
* onto given set of lines
*
* @param {Array<Point, Point>} lines
* @param {Point} point
*
* @param {Point}
*/
export function getReferencePoint(point, lines) {
if (!lines.length) {
return;
}
var nearestLine = getNearestLine(point, lines);
return perpendicularFoot(point, nearestLine);
}
/**
* Convert the given bounds to a lines array containing all edges
*
* @param {Bounds|Point} bounds
*
* @return Array<Point>
*/
export function asEdges(bounds) {
return [
[ // top
{
x: bounds.x,
y: bounds.y
},
{
x: bounds.x + (bounds.width || 0),
y: bounds.y
}
],
[ // right
{
x: bounds.x + (bounds.width || 0),
y: bounds.y
},
{
x: bounds.x + (bounds.width || 0),
y: bounds.y + (bounds.height || 0)
}
],
[ // bottom
{
x: bounds.x,
y: bounds.y + (bounds.height || 0)
},
{
x: bounds.x + (bounds.width || 0),
y: bounds.y + (bounds.height || 0)
}
],
[ // left
{
x: bounds.x,
y: bounds.y
},
{
x: bounds.x,
y: bounds.y + (bounds.height || 0)
}
]
];
}
/**
* Returns the nearest line for a given point by distance
* @param {Point} point
* @param Array<Point> lines
*
* @return Array<Point>
*/
function getNearestLine(point, lines) {
var distances = lines.map(function(l) {
return {
line: l,
distance: getDistancePointLine(point, l)
};
});
var sorted = sortBy(distances, 'distance');
return sorted[0].line;
}