diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
426 lines (321 loc) • 10.4 kB
JavaScript
import { forEach } from 'min-dash';
import {
event as domEvent,
query as domQuery,
queryAll as domQueryAll
} from 'min-dom';
import {
BENDPOINT_CLS,
SEGMENT_DRAGGER_CLS,
addBendpoint,
addSegmentDragger,
calculateSegmentMoveRegion,
getConnectionIntersection
} from './BendpointUtil';
import {
escapeCSS
} from '../../util/EscapeUtil';
import {
pointsAligned,
getMidPoint
} from '../../util/Geometry';
import {
isPrimaryButton
} from '../../util/Mouse';
import {
append as svgAppend,
attr as svgAttr,
classes as svgClasses,
create as svgCreate,
remove as svgRemove
} from 'tiny-svg';
import {
translate
} from '../../util/SvgTransformUtil';
/**
* @typedef {import('../bendpoints/BendpointMove').default} BendpointMove
* @typedef {import('../../core/Canvas').default} Canvas
* @typedef {import('../bendpoints/ConnectionSegmentMove').default} ConnectionSegmentMove
* @typedef {import('../../core/EventBus').default} EventBus
* @typedef {import('../interaction-events/InteractionEvents').default} InteractionEvents
*/
/**
* A service that adds editable bendpoints to connections.
*
* @param {EventBus} eventBus
* @param {Canvas} canvas
* @param {InteractionEvents} interactionEvents
* @param {BendpointMove} bendpointMove
* @param {ConnectionSegmentMove} connectionSegmentMove
*/
export default function Bendpoints(
eventBus, canvas, interactionEvents,
bendpointMove, connectionSegmentMove) {
/**
* Returns true if intersection point is inside middle region of segment, adjusted by
* optional threshold
*/
function isIntersectionMiddle(intersection, waypoints, treshold) {
var idx = intersection.index,
p = intersection.point,
p0, p1, mid, aligned, xDelta, yDelta;
if (idx <= 0 || intersection.bendpoint) {
return false;
}
p0 = waypoints[idx - 1];
p1 = waypoints[idx];
mid = getMidPoint(p0, p1),
aligned = pointsAligned(p0, p1);
xDelta = Math.abs(p.x - mid.x);
yDelta = Math.abs(p.y - mid.y);
return aligned && xDelta <= treshold && yDelta <= treshold;
}
/**
* Calculates the threshold from a connection's middle which fits the two-third-region
*/
function calculateIntersectionThreshold(connection, intersection) {
var waypoints = connection.waypoints,
relevantSegment, alignment, segmentLength, threshold;
if (intersection.index <= 0 || intersection.bendpoint) {
return null;
}
// segment relative to connection intersection
relevantSegment = {
start: waypoints[intersection.index - 1],
end: waypoints[intersection.index]
};
alignment = pointsAligned(relevantSegment.start, relevantSegment.end);
if (!alignment) {
return null;
}
if (alignment === 'h') {
segmentLength = relevantSegment.end.x - relevantSegment.start.x;
} else {
segmentLength = relevantSegment.end.y - relevantSegment.start.y;
}
// calculate threshold relative to 2/3 of segment length
threshold = calculateSegmentMoveRegion(segmentLength) / 2;
return threshold;
}
function activateBendpointMove(event, connection) {
var waypoints = connection.waypoints,
intersection = getConnectionIntersection(canvas, waypoints, event),
threshold;
if (!intersection) {
return;
}
threshold = calculateIntersectionThreshold(connection, intersection);
if (isIntersectionMiddle(intersection, waypoints, threshold)) {
connectionSegmentMove.start(event, connection, intersection.index);
} else {
bendpointMove.start(event, connection, intersection.index, !intersection.bendpoint);
}
// we've handled the event
return true;
}
function bindInteractionEvents(node, eventName, element) {
domEvent.bind(node, eventName, function(event) {
interactionEvents.triggerMouseEvent(eventName, event, element);
event.stopPropagation();
});
}
function getBendpointsContainer(element, create) {
var layer = canvas.getLayer('overlays'),
gfx = domQuery('.djs-bendpoints[data-element-id="' + escapeCSS(element.id) + '"]', layer);
if (!gfx && create) {
gfx = svgCreate('g');
svgAttr(gfx, { 'data-element-id': element.id });
svgClasses(gfx).add('djs-bendpoints');
svgAppend(layer, gfx);
bindInteractionEvents(gfx, 'mousedown', element);
bindInteractionEvents(gfx, 'click', element);
bindInteractionEvents(gfx, 'dblclick', element);
}
return gfx;
}
function getSegmentDragger(idx, parentGfx) {
return domQuery(
'.djs-segment-dragger[data-segment-idx="' + idx + '"]',
parentGfx
);
}
function createBendpoints(gfx, connection) {
connection.waypoints.forEach(function(p, idx) {
var bendpoint = addBendpoint(gfx);
svgAppend(gfx, bendpoint);
translate(bendpoint, p.x, p.y);
});
// add floating bendpoint
addBendpoint(gfx, 'floating');
}
function createSegmentDraggers(gfx, connection) {
var waypoints = connection.waypoints;
var segmentStart,
segmentEnd,
segmentDraggerGfx;
for (var i = 1; i < waypoints.length; i++) {
segmentStart = waypoints[i - 1];
segmentEnd = waypoints[i];
if (pointsAligned(segmentStart, segmentEnd)) {
segmentDraggerGfx = addSegmentDragger(gfx, segmentStart, segmentEnd);
svgAttr(segmentDraggerGfx, { 'data-segment-idx': i });
bindInteractionEvents(segmentDraggerGfx, 'mousemove', connection);
}
}
}
function clearBendpoints(gfx) {
forEach(domQueryAll('.' + BENDPOINT_CLS, gfx), function(node) {
svgRemove(node);
});
}
function clearSegmentDraggers(gfx) {
forEach(domQueryAll('.' + SEGMENT_DRAGGER_CLS, gfx), function(node) {
svgRemove(node);
});
}
function addHandles(connection) {
var gfx = getBendpointsContainer(connection);
if (!gfx) {
gfx = getBendpointsContainer(connection, true);
createBendpoints(gfx, connection);
createSegmentDraggers(gfx, connection);
}
return gfx;
}
function updateHandles(connection) {
var gfx = getBendpointsContainer(connection);
if (gfx) {
clearSegmentDraggers(gfx);
clearBendpoints(gfx);
createSegmentDraggers(gfx, connection);
createBendpoints(gfx, connection);
}
}
function updateFloatingBendpointPosition(parentGfx, intersection) {
var floating = domQuery('.floating', parentGfx),
point = intersection.point;
if (!floating) {
return;
}
translate(floating, point.x, point.y);
}
function updateSegmentDraggerPosition(parentGfx, intersection, waypoints) {
var draggerGfx = getSegmentDragger(intersection.index, parentGfx),
segmentStart = waypoints[intersection.index - 1],
segmentEnd = waypoints[intersection.index],
point = intersection.point,
mid = getMidPoint(segmentStart, segmentEnd),
alignment = pointsAligned(segmentStart, segmentEnd),
draggerVisual, relativePosition;
if (!draggerGfx) {
return;
}
draggerVisual = getDraggerVisual(draggerGfx);
relativePosition = {
x: point.x - mid.x,
y: point.y - mid.y
};
if (alignment === 'v') {
// rotate position
relativePosition = {
x: relativePosition.y,
y: relativePosition.x
};
}
translate(draggerVisual, relativePosition.x, relativePosition.y);
}
eventBus.on('connection.changed', function(event) {
updateHandles(event.element);
});
eventBus.on('connection.remove', function(event) {
var gfx = getBendpointsContainer(event.element);
if (gfx) {
svgRemove(gfx);
}
});
eventBus.on('element.marker.update', function(event) {
var element = event.element,
bendpointsGfx;
if (!element.waypoints) {
return;
}
bendpointsGfx = addHandles(element);
if (event.add) {
svgClasses(bendpointsGfx).add(event.marker);
} else {
svgClasses(bendpointsGfx).remove(event.marker);
}
});
eventBus.on('element.mousemove', function(event) {
var element = event.element,
waypoints = element.waypoints,
bendpointsGfx,
intersection;
if (waypoints) {
bendpointsGfx = getBendpointsContainer(element, true);
intersection = getConnectionIntersection(canvas, waypoints, event.originalEvent);
if (!intersection) {
return;
}
updateFloatingBendpointPosition(bendpointsGfx, intersection);
if (!intersection.bendpoint) {
updateSegmentDraggerPosition(bendpointsGfx, intersection, waypoints);
}
}
});
eventBus.on('element.mousedown', function(event) {
if (!isPrimaryButton(event)) {
return;
}
var originalEvent = event.originalEvent,
element = event.element;
if (!element.waypoints) {
return;
}
return activateBendpointMove(originalEvent, element);
});
eventBus.on('selection.changed', function(event) {
var newSelection = event.newSelection,
primary = newSelection[0];
if (primary && primary.waypoints) {
addHandles(primary);
}
});
eventBus.on('element.hover', function(event) {
var element = event.element;
if (element.waypoints) {
addHandles(element);
interactionEvents.registerEvent(event.gfx, 'mousemove', 'element.mousemove');
}
});
eventBus.on('element.out', function(event) {
interactionEvents.unregisterEvent(event.gfx, 'mousemove', 'element.mousemove');
});
// update bendpoint container data attribute on element ID change
eventBus.on('element.updateId', function(context) {
var element = context.element,
newId = context.newId;
if (element.waypoints) {
var bendpointContainer = getBendpointsContainer(element);
if (bendpointContainer) {
svgAttr(bendpointContainer, { 'data-element-id': newId });
}
}
});
// API
this.addHandles = addHandles;
this.updateHandles = updateHandles;
this.getBendpointsContainer = getBendpointsContainer;
this.getSegmentDragger = getSegmentDragger;
}
Bendpoints.$inject = [
'eventBus',
'canvas',
'interactionEvents',
'bendpointMove',
'connectionSegmentMove'
];
// helper /////////////
function getDraggerVisual(draggerGfx) {
return domQuery('.djs-visual', draggerGfx);
}