diagram-js
Version:
A toolbox for displaying and modifying diagrams on the web
309 lines (262 loc) • 8.31 kB
JavaScript
import {
append as svgAppend,
attr as svgAttr,
classes as svgClasses,
create as svgCreate,
remove as svgRemove,
clear as svgClear
} from 'tiny-svg';
import {
isObject
} from 'min-dash';
import {
getElementLineIntersection,
getMid
} from '../../layout/LayoutUtil';
import {
createLine
} from '../../util/RenderUtil';
/**
* @typedef {import('../../model/Types').Element} Element
* @typedef {import('../../model/Types').Connection} Connection
* @typedef {import('../../model/Types').Shape} Shape
*
* @typedef {import('../../util/Types').Point} Point
*
* @typedef {import('didi').Injector} Injector
*
* @typedef {import('../../core/Canvas').default} Canvas
* @typedef {import('../../core/ElementFactory').default} ElementFactory
* @typedef {import('../../core/GraphicsFactory').default} GraphicsFactory
*/
var MARKER_CONNECTION_PREVIEW = 'djs-dragger';
/**
* Draws connection preview. Optionally, this can use layouter and connection docking to draw
* better looking previews.
*
* @param {Injector} injector
* @param {Canvas} canvas
* @param {GraphicsFactory} graphicsFactory
* @param {ElementFactory} elementFactory
*/
export default function ConnectionPreview(
injector,
canvas,
graphicsFactory,
elementFactory
) {
this._canvas = canvas;
this._graphicsFactory = graphicsFactory;
this._elementFactory = elementFactory;
// optional components
this._connectionDocking = injector.get('connectionDocking', false);
this._layouter = injector.get('layouter', false);
}
ConnectionPreview.$inject = [
'injector',
'canvas',
'graphicsFactory',
'elementFactory'
];
/**
* Draw connection preview.
*
* Provide at least one of <source, connectionStart> and <target, connectionEnd> to create a preview.
* In the clean up stage, call `connectionPreview#cleanUp` with the context to remove preview.
*
* @param {Object} context
* @param {Object|boolean} canConnect
* @param {Object} hints
* @param {Element} [hints.source] source element
* @param {Element} [hints.target] target element
* @param {Point} [hints.connectionStart] connection preview start
* @param {Point} [hints.connectionEnd] connection preview end
* @param {Point[]} [hints.waypoints] provided waypoints for preview
* @param {boolean} [hints.noLayout] true if preview should not be laid out
* @param {boolean} [hints.noCropping] true if preview should not be cropped
* @param {boolean} [hints.noNoop] true if simple connection should not be drawn
*/
ConnectionPreview.prototype.drawPreview = function(context, canConnect, hints) {
hints = hints || {};
var connectionPreviewGfx = context.connectionPreviewGfx,
getConnection = context.getConnection,
source = hints.source,
target = hints.target,
waypoints = hints.waypoints,
connectionStart = hints.connectionStart,
connectionEnd = hints.connectionEnd,
noLayout = hints.noLayout,
noCropping = hints.noCropping,
noNoop = hints.noNoop,
connection;
var self = this;
if (!connectionPreviewGfx) {
connectionPreviewGfx = context.connectionPreviewGfx = this.createConnectionPreviewGfx();
}
svgClear(connectionPreviewGfx);
if (!getConnection) {
getConnection = context.getConnection = cacheReturnValues(function(canConnect, source, target) {
return self.getConnection(canConnect, source, target);
});
}
if (canConnect) {
connection = getConnection(canConnect, source, target);
}
if (!connection) {
!noNoop && this.drawNoopPreview(connectionPreviewGfx, hints);
return;
}
connection.waypoints = waypoints || [];
// optional layout
if (this._layouter && !noLayout) {
connection.waypoints = this._layouter.layoutConnection(connection, {
source: source,
target: target,
connectionStart: connectionStart,
connectionEnd: connectionEnd,
waypoints: hints.waypoints || connection.waypoints
});
}
// fallback if no waypoints were provided nor created with layouter
if (!connection.waypoints || !connection.waypoints.length) {
connection.waypoints = [
source ? getMid(source) : connectionStart,
target ? getMid(target) : connectionEnd
];
}
// optional cropping
if (this._connectionDocking && (source || target) && !noCropping) {
connection.waypoints = this._connectionDocking.getCroppedWaypoints(connection, source, target);
}
this._graphicsFactory.drawConnection(connectionPreviewGfx, connection, {
stroke: 'var(--element-dragger-color)'
});
};
/**
* Draw simple connection between source and target or provided points.
*
* @param {SVGElement} connectionPreviewGfx container for the connection
* @param {Object} hints
* @param {Element} [hints.source] source element
* @param {Element} [hints.target] target element
* @param {Point} [hints.connectionStart] required if source is not provided
* @param {Point} [hints.connectionEnd] required if target is not provided
*/
ConnectionPreview.prototype.drawNoopPreview = function(connectionPreviewGfx, hints) {
var source = hints.source,
target = hints.target,
start = hints.connectionStart || getMid(source),
end = hints.connectionEnd || getMid(target);
var waypoints = this.cropWaypoints(start, end, source, target);
var connection = this.createNoopConnection(waypoints[0], waypoints[1]);
svgAppend(connectionPreviewGfx, connection);
};
/**
* Return cropped waypoints.
*
* @param {Point} start
* @param {Point} end
* @param {Element} source
* @param {Element} target
*
* @return {Point[]}
*/
ConnectionPreview.prototype.cropWaypoints = function(start, end, source, target) {
var graphicsFactory = this._graphicsFactory,
sourcePath = source && graphicsFactory.getShapePath(source),
targetPath = target && graphicsFactory.getShapePath(target),
connectionPath = graphicsFactory.getConnectionPath({ waypoints: [ start, end ] });
start = (source && getElementLineIntersection(sourcePath, connectionPath, true)) || start;
end = (target && getElementLineIntersection(targetPath, connectionPath, false)) || end;
return [ start, end ];
};
/**
* Remove connection preview container if it exists.
*
* @param {Object} [context]
* @param {SVGElement} [context.connectionPreviewGfx] preview container
*/
ConnectionPreview.prototype.cleanUp = function(context) {
if (context && context.connectionPreviewGfx) {
svgRemove(context.connectionPreviewGfx);
}
};
/**
* Get connection that connects source and target.
*
* @param {Object|boolean} canConnect
*
* @return {Connection}
*/
ConnectionPreview.prototype.getConnection = function(canConnect) {
var attrs = ensureConnectionAttrs(canConnect);
return this._elementFactory.createConnection(attrs);
};
/**
* Add and return preview graphics.
*
* @return {SVGElement}
*/
ConnectionPreview.prototype.createConnectionPreviewGfx = function() {
var gfx = svgCreate('g');
svgAttr(gfx, {
pointerEvents: 'none'
});
svgClasses(gfx).add(MARKER_CONNECTION_PREVIEW);
svgAppend(this._canvas.getActiveLayer(), gfx);
return gfx;
};
/**
* Create and return simple connection.
*
* @param {Point} start
* @param {Point} end
*
* @return {SVGElement}
*/
ConnectionPreview.prototype.createNoopConnection = function(start, end) {
return createLine([ start, end ], {
'stroke': '#333',
'strokeDasharray': [ 1 ],
'strokeWidth': 2,
'pointer-events': 'none'
});
};
// helpers //////////
/**
* Returns function that returns cached return values referenced by stringified first argument.
*
* @param {Function} fn
*
* @return {Function}
*/
function cacheReturnValues(fn) {
var returnValues = {};
/**
* Return cached return value referenced by stringified first argument.
*
* @return {*}
*/
return function(firstArgument) {
var key = JSON.stringify(firstArgument);
var returnValue = returnValues[key];
if (!returnValue) {
returnValue = returnValues[key] = fn.apply(null, arguments);
}
return returnValue;
};
}
/**
* Ensure connection attributes is object.
*
* @param {Object|boolean} canConnect
*
* @return {Object}
*/
function ensureConnectionAttrs(canConnect) {
if (isObject(canConnect)) {
return canConnect;
} else {
return {};
}
}