import { assign } from 'min-dash'; import inherits from 'inherits'; import { is, getBusinessObject, getDi } 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, labelTarget = context.labelTarget, di; // we want to trigger on real labels only if (!labelTarget) { return; } // we want to trigger on BPMN elements only if (!is(labelTarget, 'bpmn:BaseElement')) { return; } di = getDi(labelTarget); if (!di.label) { di.label = bpmnFactory.create('bpmndi:BPMNLabel', { bounds: bpmnFactory.create('dc:Bounds') }); element.di = di; } 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} 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 */ 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 lines * * @return Array */ 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; }