@antv/x6
Version:
JavaScript diagramming library that uses SVG and HTML for rendering
1,359 lines • 69.8 kB
JavaScript
import { __rest } from "tslib";
import { Dom, FunctionExt, IS_SAFARI, NumberExt, ObjectExt, Vector, } from '../../common';
import { Line, normalize, Path, Point, Polyline, Rectangle, } from '../../geometry';
import { Edge, } from '../../model/edge';
import { connectionPointRegistry, connectorPresets, connectorRegistry, edgeAnchorRegistry, nodeAnchorRegistry, routerPresets, routerRegistry, } from '../../registry';
import { CellView } from '../cell';
import { NodeView } from '../node';
export * from './type';
export class EdgeView extends CellView {
constructor() {
super(...arguments);
this.POINT_ROUNDING = 2;
this.labelDestroyFn = {};
// #endregion
}
static isEdgeView(instance) {
if (instance == null) {
return false;
}
if (instance instanceof EdgeView) {
return true;
}
const tag = instance[Symbol.toStringTag];
const view = instance;
if ((tag == null || tag === EdgeViewToStringTag) &&
typeof view.isNodeView === 'function' &&
typeof view.isEdgeView === 'function' &&
typeof view.confirmUpdate === 'function' &&
typeof view.update === 'function' &&
typeof view.getConnection === 'function') {
return true;
}
return false;
}
get [Symbol.toStringTag]() {
return EdgeViewToStringTag;
}
getContainerClassName() {
return [super.getContainerClassName(), this.prefixClassName('edge')].join(' ');
}
get sourceBBox() {
const sourceView = this.sourceView;
if (!sourceView || !this.graph.renderer.isViewMounted(sourceView)) {
const sourceCell = this.cell.getSourceCell();
if (sourceCell) {
return sourceCell.getBBox();
}
const sourcePoint = this.cell.getSourcePoint();
return new Rectangle(sourcePoint.x, sourcePoint.y);
}
const sourceMagnet = this.sourceMagnet;
if (sourceView.isEdgeElement(sourceMagnet)) {
return new Rectangle(this.sourceAnchor.x, this.sourceAnchor.y);
}
return sourceView.getBBoxOfElement(sourceMagnet || sourceView.container);
}
get targetBBox() {
const targetView = this.targetView;
if (!targetView || !this.graph.renderer.isViewMounted(targetView)) {
const targetCell = this.cell.getTargetCell();
if (targetCell) {
return targetCell.getBBox();
}
const targetPoint = this.cell.getTargetPoint();
return new Rectangle(targetPoint.x, targetPoint.y);
}
const targetMagnet = this.targetMagnet;
if (targetView.isEdgeElement(targetMagnet)) {
return new Rectangle(this.targetAnchor.x, this.targetAnchor.y);
}
return targetView.getBBoxOfElement(targetMagnet || targetView.container);
}
isEdgeView() {
return true;
}
confirmUpdate(flag, options = {}) {
let ref = flag;
if (this.hasAction(ref, 'source')) {
if (!this.updateTerminalProperties('source')) {
return ref;
}
ref = this.removeAction(ref, 'source');
}
if (this.hasAction(ref, 'target')) {
if (!this.updateTerminalProperties('target')) {
return ref;
}
ref = this.removeAction(ref, 'target');
}
if (this.hasAction(ref, 'render')) {
this.render();
ref = this.removeAction(ref, ['render', 'update', 'labels', 'tools']);
return ref;
}
ref = this.handleAction(ref, 'update', () => this.update(options));
ref = this.handleAction(ref, 'labels', () => this.onLabelsChange(options));
ref = this.handleAction(ref, 'tools', () => this.renderTools());
return ref;
}
// #region render
render() {
this.empty();
this.renderMarkup();
this.labelContainer = null;
this.renderLabels();
this.update();
this.renderTools();
this.notify('view:render', { view: this });
return this;
}
renderMarkup() {
const markup = this.cell.markup;
if (markup) {
if (typeof markup === 'string') {
throw new TypeError('Not support string markup.');
}
return this.renderJSONMarkup(markup);
}
throw new TypeError('Invalid edge markup.');
}
renderJSONMarkup(markup) {
const ret = this.parseJSONMarkup(markup, this.container);
this.selectors = ret.selectors;
this.container.append(ret.fragment);
}
customizeLabels() {
if (this.labelContainer) {
const edge = this.cell;
const labels = edge.labels;
for (let i = 0, n = labels.length; i < n; i += 1) {
const label = labels[i];
const container = this.labelCache[i];
const selectors = this.labelSelectors[i];
const onEdgeLabelRendered = this.graph.options.onEdgeLabelRendered;
if (onEdgeLabelRendered) {
const fn = onEdgeLabelRendered({
edge,
label,
container,
selectors,
});
if (fn) {
this.labelDestroyFn[i] = fn;
}
}
}
}
}
destroyCustomizeLabels() {
const labels = this.cell.labels;
if (this.labelCache && this.labelSelectors && this.labelDestroyFn) {
for (let i = 0, n = labels.length; i < n; i += 1) {
const fn = this.labelDestroyFn[i];
const container = this.labelCache[i];
const selectors = this.labelSelectors[i];
if (fn && container && selectors) {
fn({
edge: this.cell,
label: labels[i],
container,
selectors,
});
}
}
}
this.labelDestroyFn = {};
}
renderLabels() {
const edge = this.cell;
const labels = edge.getLabels();
const count = labels.length;
let container = this.labelContainer;
this.labelCache = {};
this.labelSelectors = {};
if (count <= 0) {
if (container === null || container === void 0 ? void 0 : container.parentNode) {
container.parentNode.removeChild(container);
}
return this;
}
if (container) {
this.empty(container);
}
else {
container = Dom.createSvgElement('g');
this.addClass(this.prefixClassName('edge-labels'), container);
this.labelContainer = container;
}
for (let i = 0, ii = labels.length; i < ii; i += 1) {
const label = labels[i];
const normalized = this.normalizeLabelMarkup(this.parseLabelMarkup(label.markup));
let labelNode;
let selectors;
if (normalized) {
labelNode = normalized.node;
selectors = normalized.selectors;
}
else {
const defaultLabel = edge.getDefaultLabel();
const normalized = this.normalizeLabelMarkup(this.parseLabelMarkup(defaultLabel.markup));
labelNode = normalized.node;
selectors = normalized.selectors;
}
labelNode.setAttribute('data-index', `${i}`);
container.appendChild(labelNode);
const rootSelector = this.rootSelector;
if (selectors[rootSelector]) {
throw new Error('Ambiguous label root selector.');
}
selectors[rootSelector] = labelNode;
this.labelCache[i] = labelNode;
this.labelSelectors[i] = selectors;
}
if (container.parentNode == null) {
this.container.appendChild(container);
}
this.updateLabels();
this.customizeLabels();
return this;
}
onLabelsChange(options = {}) {
this.destroyCustomizeLabels();
if (this.shouldRerenderLabels(options)) {
this.renderLabels();
}
else {
this.updateLabels();
}
this.updateLabelPositions();
}
shouldRerenderLabels(options = {}) {
const previousLabels = this.cell.previous('labels');
if (previousLabels == null) {
return true;
}
// Here is an optimization for cases when we know, that change does
// not require re-rendering of all labels.
if ('propertyPathArray' in options && 'propertyValue' in options) {
// The label is setting by `prop()` method
const pathArray = options.propertyPathArray || [];
const pathLength = pathArray.length;
if (pathLength > 1) {
// We are changing a single label here e.g. 'labels/0/position'
const index = pathArray[1];
if (previousLabels[index]) {
if (pathLength === 2) {
// We are changing the entire label. Need to check if the
// markup is also being changed.
return (typeof options.propertyValue === 'object' &&
ObjectExt.has(options.propertyValue, 'markup'));
}
// We are changing a label property but not the markup
if (pathArray[2] !== 'markup') {
return false;
}
}
}
}
return true;
}
parseLabelMarkup(markup) {
if (markup) {
if (typeof markup === 'string') {
return this.parseLabelStringMarkup(markup);
}
return this.parseJSONMarkup(markup);
}
return null;
}
parseLabelStringMarkup(labelMarkup) {
const children = Vector.createVectors(labelMarkup);
const fragment = document.createDocumentFragment();
for (let i = 0, n = children.length; i < n; i += 1) {
const currentChild = children[i].node;
fragment.appendChild(currentChild);
}
return { fragment, selectors: {} };
}
normalizeLabelMarkup(markup) {
if (markup == null) {
return;
}
const fragment = markup.fragment;
if (!(fragment instanceof DocumentFragment) || !fragment.hasChildNodes()) {
throw new Error('Invalid label markup.');
}
let vel;
const childNodes = fragment.childNodes;
if (childNodes.length > 1 || childNodes[0].nodeName.toUpperCase() !== 'G') {
vel = Vector.create('g').append(fragment);
}
else {
vel = Vector.create(childNodes[0]);
}
vel.addClass(this.prefixClassName('edge-label'));
return {
node: vel.node,
selectors: markup.selectors,
};
}
updateLabels() {
if (this.labelContainer) {
const edge = this.cell;
const labels = edge.labels;
const canLabelMove = this.can('edgeLabelMovable');
const defaultLabel = edge.getDefaultLabel();
for (let i = 0, n = labels.length; i < n; i += 1) {
const elem = this.labelCache[i];
const selectors = this.labelSelectors[i];
elem.setAttribute('cursor', canLabelMove ? 'move' : 'default');
const label = labels[i];
const attrs = ObjectExt.merge({}, defaultLabel.attrs, label.attrs);
this.updateAttrs(elem, attrs, {
selectors,
rootBBox: label.size ? Rectangle.fromSize(label.size) : undefined,
});
}
}
}
renderTools() {
const tools = this.cell.getTools();
this.addTools(tools);
return this;
}
// #endregion
// #region updating
update(options = {}) {
var _a;
this.cleanCache();
this.updateConnection(options);
const _b = this.cell.getAttrs(), { text } = _b, attrs = __rest(_b, ["text"]);
if (attrs != null) {
// FIXME: safari 兼容,重新渲染一次edge 的g元素,确保重排/重绘能渲染出marker
if (((_a = this.container) === null || _a === void 0 ? void 0 : _a.tagName) === 'g' &&
this.isEdgeElement(this.container) &&
IS_SAFARI) {
const parent = this.container.parentNode;
if (parent) {
const next = this.container.nextSibling;
parent.removeChild(this.container);
parent.insertBefore(this.container, next);
}
}
this.updateAttrs(this.container, attrs, {
selectors: this.selectors,
});
}
this.updateLabelPositions();
this.updateTools(options);
return this;
}
removeRedundantLinearVertices(options = {}) {
const edge = this.cell;
const vertices = edge.getVertices();
const routePoints = [this.sourceAnchor, ...vertices, this.targetAnchor];
const rawCount = routePoints.length;
// Puts the route points into a polyline and try to simplify.
const polyline = new Polyline(routePoints);
polyline.simplify({ threshold: 0.01 });
const simplifiedPoints = polyline.points.map((point) => point.toJSON());
const simplifiedCount = simplifiedPoints.length;
// If simplification did not remove any redundant vertices.
if (rawCount === simplifiedCount) {
return 0;
}
// Sets simplified polyline points as edge vertices.
// Removes first and last polyline points again (source/target anchors).
edge.setVertices(simplifiedPoints.slice(1, simplifiedCount - 1), options);
return rawCount - simplifiedCount;
}
getTerminalView(type) {
switch (type) {
case 'source':
return this.sourceView || null;
case 'target':
return this.targetView || null;
default:
throw new Error(`Unknown terminal type '${type}'`);
}
}
getTerminalAnchor(type) {
switch (type) {
case 'source':
return Point.create(this.sourceAnchor);
case 'target':
return Point.create(this.targetAnchor);
default:
throw new Error(`Unknown terminal type '${type}'`);
}
}
getTerminalConnectionPoint(type) {
switch (type) {
case 'source':
return Point.create(this.sourcePoint);
case 'target':
return Point.create(this.targetPoint);
default:
throw new Error(`Unknown terminal type '${type}'`);
}
}
getTerminalMagnet(type, options = {}) {
switch (type) {
case 'source': {
if (options.raw) {
return this.sourceMagnet;
}
const sourceView = this.sourceView;
if (!sourceView) {
return null;
}
return this.sourceMagnet || sourceView.container;
}
case 'target': {
if (options.raw) {
return this.targetMagnet;
}
const targetView = this.targetView;
if (!targetView) {
return null;
}
return this.targetMagnet || targetView.container;
}
default: {
throw new Error(`Unknown terminal type '${type}'`);
}
}
}
updateConnection(options = {}) {
const edge = this.cell;
// The edge is being translated by an ancestor that will shift
// source, target and vertices by an equal distance.
// todo isFragmentDescendantOf is invalid
if (options.translateBy &&
edge.isFragmentDescendantOf(options.translateBy)) {
const tx = options.tx || 0;
const ty = options.ty || 0;
this.routePoints = new Polyline(this.routePoints).translate(tx, ty).points;
this.translateConnectionPoints(tx, ty);
this.path.translate(tx, ty);
}
else {
const vertices = edge.getVertices();
// 1. Find anchor points
const anchors = this.findAnchors(vertices);
this.sourceAnchor = anchors.source;
this.targetAnchor = anchors.target;
// 2. Find route points
this.routePoints = this.findRoutePoints(vertices);
// 3. Find connection points
const connectionPoints = this.findConnectionPoints(this.routePoints, this.sourceAnchor, this.targetAnchor);
this.sourcePoint = connectionPoints.source;
this.targetPoint = connectionPoints.target;
// 4. Find Marker Connection Point
const markerPoints = this.findMarkerPoints(this.routePoints, this.sourcePoint, this.targetPoint);
// 5. Make path
this.path = this.findPath(this.routePoints, markerPoints.source || this.sourcePoint, markerPoints.target || this.targetPoint);
}
this.cleanCache();
}
findAnchors(vertices) {
const edge = this.cell;
const source = edge.source;
const target = edge.target;
const firstVertex = vertices[0];
const lastVertex = vertices[vertices.length - 1];
if (target.priority && !source.priority) {
// Reversed order
return this.findAnchorsOrdered('target', lastVertex, 'source', firstVertex);
}
// Usual order
return this.findAnchorsOrdered('source', firstVertex, 'target', lastVertex);
}
findAnchorsOrdered(firstType, firstPoint, secondType, secondPoint) {
let firstAnchor;
let secondAnchor;
const edge = this.cell;
const firstTerminal = edge[firstType];
const secondTerminal = edge[secondType];
const firstView = this.getTerminalView(firstType);
const secondView = this.getTerminalView(secondType);
const firstMagnet = this.getTerminalMagnet(firstType);
const secondMagnet = this.getTerminalMagnet(secondType);
if (firstView) {
let firstRef;
if (firstPoint) {
firstRef = Point.create(firstPoint);
}
else if (secondView) {
firstRef = secondMagnet;
}
else {
firstRef = Point.create(secondTerminal);
}
firstAnchor = this.getAnchor(firstTerminal.anchor, firstView, firstMagnet, firstRef, firstType);
}
else {
firstAnchor = Point.create(firstTerminal);
}
if (secondView) {
const secondRef = Point.create(secondPoint || firstAnchor);
secondAnchor = this.getAnchor(secondTerminal.anchor, secondView, secondMagnet, secondRef, secondType);
}
else {
secondAnchor = Point.isPointLike(secondTerminal)
? Point.create(secondTerminal)
: new Point();
}
return {
[firstType]: firstAnchor,
[secondType]: secondAnchor,
};
}
getAnchor(def, cellView, magnet, ref, terminalType) {
const isEdge = cellView.isEdgeElement(magnet);
const connecting = this.graph.options.connecting;
let config = typeof def === 'string' ? { name: def } : def;
if (!config) {
const defaults = isEdge
? (terminalType === 'source'
? connecting.sourceEdgeAnchor
: connecting.targetEdgeAnchor) || connecting.edgeAnchor
: (terminalType === 'source'
? connecting.sourceAnchor
: connecting.targetAnchor) || connecting.anchor;
config = typeof defaults === 'string' ? { name: defaults } : defaults;
}
if (!config) {
throw new Error(`Anchor should be specified.`);
}
let anchor;
const name = config.name;
if (isEdge) {
const fn = edgeAnchorRegistry.get(name);
if (typeof fn !== 'function') {
return edgeAnchorRegistry.onNotFound(name);
}
anchor = FunctionExt.call(fn, this, cellView, magnet, ref, config.args || {}, terminalType);
}
else {
const fn = nodeAnchorRegistry.get(name);
if (typeof fn !== 'function') {
return nodeAnchorRegistry.onNotFound(name);
}
anchor = FunctionExt.call(fn, this, cellView, magnet, ref, config.args || {}, terminalType);
}
return anchor ? anchor.round(this.POINT_ROUNDING) : new Point();
}
findRoutePoints(vertices = []) {
const defaultRouter = this.graph.options.connecting.router || routerPresets.normal;
const router = this.cell.getRouter() || defaultRouter;
let routePoints;
if (typeof router === 'function') {
routePoints = FunctionExt.call(router, this, vertices, {}, this);
}
else {
const name = typeof router === 'string' ? router : router.name;
const args = typeof router === 'string' ? {} : router.args || {};
const fn = name ? routerRegistry.get(name) : routerPresets.normal;
if (typeof fn !== 'function') {
return routerRegistry.onNotFound(name);
}
routePoints = FunctionExt.call(fn, this, vertices, args, this);
}
return routePoints == null
? vertices.map((p) => Point.create(p))
: routePoints.map((p) => Point.create(p));
}
findConnectionPoints(routePoints, sourceAnchor, targetAnchor) {
const edge = this.cell;
const connecting = this.graph.options.connecting;
const sourceTerminal = edge.getSource();
const targetTerminal = edge.getTarget();
const sourceView = this.sourceView;
const targetView = this.targetView;
const firstRoutePoint = routePoints[0];
const lastRoutePoint = routePoints[routePoints.length - 1];
// source
let sourcePoint;
if (sourceView && !sourceView.isEdgeElement(this.sourceMagnet)) {
const sourceMagnet = this.sourceMagnet || sourceView.container;
const sourcePointRef = firstRoutePoint || targetAnchor;
const sourceLine = new Line(sourcePointRef, sourceAnchor);
const connectionPointDef = sourceTerminal.connectionPoint ||
connecting.sourceConnectionPoint ||
connecting.connectionPoint;
sourcePoint = this.getConnectionPoint(connectionPointDef, sourceView, sourceMagnet, sourceLine, 'source');
}
else {
sourcePoint = sourceAnchor;
}
// target
let targetPoint;
if (targetView && !targetView.isEdgeElement(this.targetMagnet)) {
const targetMagnet = this.targetMagnet || targetView.container;
const targetConnectionPointDef = targetTerminal.connectionPoint ||
connecting.targetConnectionPoint ||
connecting.connectionPoint;
const targetPointRef = lastRoutePoint || sourceAnchor;
const targetLine = new Line(targetPointRef, targetAnchor);
targetPoint = this.getConnectionPoint(targetConnectionPointDef, targetView, targetMagnet, targetLine, 'target');
}
else {
targetPoint = targetAnchor;
}
return {
source: sourcePoint,
target: targetPoint,
};
}
getConnectionPoint(def, view, magnet, line, endType) {
const anchor = line.end;
if (def == null) {
return anchor;
}
const name = typeof def === 'string' ? def : def.name;
const args = typeof def === 'string' ? {} : def.args;
const fn = connectionPointRegistry.get(name);
if (typeof fn !== 'function') {
return connectionPointRegistry.onNotFound(name);
}
const connectionPoint = FunctionExt.call(fn, this, line, view, magnet, args || {}, endType);
return connectionPoint ? connectionPoint.round(this.POINT_ROUNDING) : anchor;
}
findMarkerPoints(routePoints, sourcePoint, targetPoint) {
const getLineWidth = (type) => {
const attrs = this.cell.getAttrs();
const keys = Object.keys(attrs);
for (let i = 0, l = keys.length; i < l; i += 1) {
const attr = attrs[keys[i]];
if (attr[`${type}Marker`] || attr[`${type}-marker`]) {
const strokeWidth = attr.strokeWidth || attr['stroke-width'];
if (strokeWidth) {
return parseFloat(strokeWidth);
}
break;
}
}
return null;
};
const firstRoutePoint = routePoints[0];
const lastRoutePoint = routePoints[routePoints.length - 1];
let sourceMarkerPoint;
let targetMarkerPoint;
const sourceStrokeWidth = getLineWidth('source');
if (sourceStrokeWidth) {
sourceMarkerPoint = sourcePoint
.clone()
.move(firstRoutePoint || targetPoint, -sourceStrokeWidth);
}
const targetStrokeWidth = getLineWidth('target');
if (targetStrokeWidth) {
targetMarkerPoint = targetPoint
.clone()
.move(lastRoutePoint || sourcePoint, -targetStrokeWidth);
}
this.sourceMarkerPoint = sourceMarkerPoint || sourcePoint.clone();
this.targetMarkerPoint = targetMarkerPoint || targetPoint.clone();
return {
source: sourceMarkerPoint,
target: targetMarkerPoint,
};
}
findPath(routePoints, sourcePoint, targetPoint) {
const def = this.cell.getConnector() || this.graph.options.connecting.connector;
let name;
let args;
let fn;
if (typeof def === 'string') {
name = def;
}
else {
name = def.name;
args = def.args;
}
if (name) {
const method = connectorRegistry.get(name);
if (typeof method !== 'function') {
return connectorRegistry.onNotFound(name);
}
fn = method;
}
else {
fn = connectorPresets.normal;
}
const path = FunctionExt.call(fn, this, sourcePoint, targetPoint, routePoints, Object.assign(Object.assign({}, args), { raw: true }), this);
return typeof path === 'string' ? Path.parse(path) : path;
}
translateConnectionPoints(tx, ty) {
this.sourcePoint.translate(tx, ty);
this.targetPoint.translate(tx, ty);
this.sourceAnchor.translate(tx, ty);
this.targetAnchor.translate(tx, ty);
this.sourceMarkerPoint.translate(tx, ty);
this.targetMarkerPoint.translate(tx, ty);
}
updateLabelPositions() {
if (this.labelContainer == null) {
return this;
}
const path = this.path;
if (!path) {
return this;
}
const edge = this.cell;
const labels = edge.getLabels();
if (labels.length === 0) {
return this;
}
const defaultLabel = edge.getDefaultLabel();
const defaultPosition = this.normalizeLabelPosition(defaultLabel.position);
for (let i = 0, ii = labels.length; i < ii; i += 1) {
const label = labels[i];
const labelNode = this.labelCache[i];
if (!labelNode) {
continue;
}
const labelPosition = this.normalizeLabelPosition(label.position);
const pos = ObjectExt.merge({}, defaultPosition, labelPosition);
const matrix = this.getLabelTransformationMatrix(pos);
labelNode.setAttribute('transform', Dom.matrixToTransformString(matrix));
}
return this;
}
updateTerminalProperties(type) {
const edge = this.cell;
const graph = this.graph;
const terminal = edge[type];
const nodeId = terminal && terminal.cell;
const viewKey = `${type}View`;
// terminal is a point
if (!nodeId) {
this[viewKey] = null;
this.updateTerminalMagnet(type);
return true;
}
const terminalCell = graph.getCellById(nodeId);
if (!terminalCell) {
throw new Error(`Edge's ${type} node with id "${nodeId}" not exists`);
}
const endView = terminalCell.findView(graph);
if (!endView) {
return false;
}
this[viewKey] = endView;
this.updateTerminalMagnet(type);
return true;
}
updateTerminalMagnet(type) {
const propName = `${type}Magnet`;
const terminalView = this.getTerminalView(type);
if (terminalView) {
let magnet = terminalView.getMagnetFromEdgeTerminal(this.cell[type]);
if (magnet === terminalView.container) {
magnet = null;
}
this[propName] = magnet;
}
else {
this[propName] = null;
}
}
getLabelPositionAngle(idx) {
const label = this.cell.getLabelAt(idx);
if (label && label.position && typeof label.position === 'object') {
return label.position.angle || 0;
}
return 0;
}
getLabelPositionArgs(idx) {
const label = this.cell.getLabelAt(idx);
if (label && label.position && typeof label.position === 'object') {
return label.position.options;
}
}
getDefaultLabelPositionArgs() {
const defaultLabel = this.cell.getDefaultLabel();
if (defaultLabel &&
defaultLabel.position &&
typeof defaultLabel.position === 'object') {
return defaultLabel.position.options;
}
}
mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs) {
if (labelPositionArgs === null) {
return null;
}
if (labelPositionArgs === undefined) {
if (defaultLabelPositionArgs === null) {
return null;
}
return defaultLabelPositionArgs;
}
return ObjectExt.merge({}, defaultLabelPositionArgs, labelPositionArgs);
}
// #endregion
getConnection() {
return this.path != null ? this.path.clone() : null;
}
getConnectionPathData() {
if (this.path == null) {
return '';
}
const cache = this.cache.pathCache;
if (!ObjectExt.has(cache, 'data')) {
cache.data = this.path.serialize();
}
return cache.data || '';
}
getConnectionSubdivisions() {
if (this.path == null) {
return null;
}
const cache = this.cache.pathCache;
if (!ObjectExt.has(cache, 'segmentSubdivisions')) {
cache.segmentSubdivisions = this.path.getSegmentSubdivisions();
}
return cache.segmentSubdivisions;
}
getConnectionLength() {
if (this.path == null) {
return 0;
}
const cache = this.cache.pathCache;
if (!ObjectExt.has(cache, 'length')) {
cache.length = this.path.length({
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
return cache.length;
}
getPointAtLength(length) {
if (this.path == null) {
return null;
}
return this.path.pointAtLength(length, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getPointAtRatio(ratio) {
if (this.path == null) {
return null;
}
if (NumberExt.isPercentage(ratio)) {
// eslint-disable-next-line
ratio = parseFloat(ratio) / 100;
}
return this.path.pointAt(ratio, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getTangentAtLength(length) {
if (this.path == null) {
return null;
}
return this.path.tangentAtLength(length, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getTangentAtRatio(ratio) {
if (this.path == null) {
return null;
}
return this.path.tangentAt(ratio, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getClosestPoint(point) {
if (this.path == null) {
return null;
}
return this.path.closestPoint(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getClosestPointLength(point) {
if (this.path == null) {
return null;
}
return this.path.closestPointLength(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getClosestPointRatio(point) {
if (this.path == null) {
return null;
}
return this.path.closestPointNormalizedLength(point, {
segmentSubdivisions: this.getConnectionSubdivisions(),
});
}
getLabelPosition(x, y, p3, p4) {
var _a;
const pos = { distance: 0 };
// normalize data from the two possible signatures
let angle = 0;
let options;
if (typeof p3 === 'number') {
angle = p3;
options = p4;
}
else {
options = p3;
}
if (options != null) {
pos.options = options;
}
// identify distance/offset settings
const isOffsetAbsolute = options === null || options === void 0 ? void 0 : options.absoluteOffset;
const isDistanceRelative = !(options === null || options === void 0 ? void 0 : options.absoluteDistance);
const isDistanceAbsoluteReverse = (options === null || options === void 0 ? void 0 : options.absoluteDistance) && options.reverseDistance;
// find closest point t
const path = this.path;
const pathOptions = {
segmentSubdivisions: this.getConnectionSubdivisions(),
};
const labelPoint = new Point(x, y);
const t = (_a = path.closestPointT(labelPoint, pathOptions)) !== null && _a !== void 0 ? _a : 0;
// distance
const totalLength = this.getConnectionLength() || 0;
let labelDistance = path.lengthAtT(t, pathOptions);
if (isDistanceRelative) {
labelDistance = totalLength > 0 ? labelDistance / totalLength : 0;
}
if (isDistanceAbsoluteReverse) {
// fix for end point (-0 => 1)
labelDistance = -1 * (totalLength - labelDistance) || 1;
}
pos.distance = labelDistance;
// offset
// use absolute offset if:
// - options.absoluteOffset is true,
// - options.absoluteOffset is not true but there is no tangent
let tangent;
if (!isOffsetAbsolute)
tangent = path.tangentAtT(t);
let labelOffset;
if (tangent) {
labelOffset = tangent.pointOffset(labelPoint);
}
else {
const closestPoint = path.pointAtT(t);
if (closestPoint) {
const labelOffsetDiff = labelPoint.diff(closestPoint);
labelOffset = { x: labelOffsetDiff.x, y: labelOffsetDiff.y };
}
else {
labelOffset = { x: 0, y: 0 };
}
}
pos.offset = labelOffset;
pos.angle = angle;
return pos;
}
normalizeLabelPosition(pos) {
if (typeof pos === 'number') {
return { distance: pos };
}
return pos;
}
getLabelTransformationMatrix(labelPosition) {
const pos = this.normalizeLabelPosition(labelPosition);
const options = pos.options || {};
const labelAngle = pos.angle || 0;
const labelDistance = pos.distance;
const isDistanceRelative = labelDistance > 0 && labelDistance <= 1;
let labelOffset = 0;
const offsetCoord = { x: 0, y: 0 };
const offset = pos.offset;
if (offset) {
if (typeof offset === 'number') {
labelOffset = offset;
}
else {
if (offset.x != null) {
offsetCoord.x = offset.x;
}
if (offset.y != null) {
offsetCoord.y = offset.y;
}
}
}
const isOffsetAbsolute = offsetCoord.x !== 0 || offsetCoord.y !== 0 || labelOffset === 0;
const isKeepGradient = options.keepGradient;
const isEnsureLegibility = options.ensureLegibility;
const path = this.path;
const pathOpt = { segmentSubdivisions: this.getConnectionSubdivisions() };
const distance = isDistanceRelative
? labelDistance * (this.getConnectionLength() || 0)
: labelDistance;
const tangent = path.tangentAtLength(distance, pathOpt);
let translation;
let angle = labelAngle;
if (tangent) {
if (isOffsetAbsolute) {
translation = tangent.start;
translation.translate(offsetCoord);
}
else {
const normal = tangent.clone();
normal.rotate(-90, tangent.start);
normal.setLength(labelOffset);
translation = normal.end;
}
if (isKeepGradient) {
angle = tangent.angle() + labelAngle;
if (isEnsureLegibility) {
angle = normalize(((angle + 90) % 180) - 90);
}
}
}
else {
// fallback - the connection has zero length
translation = path.pointAtLength(0, pathOpt) || new Point();
if (isOffsetAbsolute) {
translation.translate(offsetCoord);
}
}
return Dom.createSVGMatrix()
.translate(translation.x, translation.y)
.rotate(angle);
}
getVertexIndex(x, y) {
const edge = this.cell;
const vertices = edge.getVertices();
const vertexLength = this.getClosestPointLength(new Point(x, y));
let index = 0;
if (vertexLength != null) {
for (const ii = vertices.length; index < ii; index += 1) {
const currentVertex = vertices[index];
const currentLength = this.getClosestPointLength(currentVertex);
if (currentLength != null && vertexLength < currentLength) {
break;
}
}
}
return index;
}
getEventArgs(e, x, y) {
const view = this; // eslint-disable-line
const edge = view.cell;
const cell = edge;
if (x == null || y == null) {
return { e, view, edge, cell };
}
return { e, x, y, view, edge, cell };
}
notifyUnhandledMouseDown(e, x, y) {
this.notify('edge:unhandled:mousedown', {
e,
x,
y,
view: this,
cell: this.cell,
edge: this.cell,
});
}
notifyMouseDown(e, x, y) {
super.onMouseDown(e, x, y);
this.notify('edge:mousedown', this.getEventArgs(e, x, y));
}
notifyMouseMove(e, x, y) {
super.onMouseMove(e, x, y);
this.notify('edge:mousemove', this.getEventArgs(e, x, y));
}
notifyMouseUp(e, x, y) {
super.onMouseUp(e, x, y);
this.notify('edge:mouseup', this.getEventArgs(e, x, y));
}
onClick(e, x, y) {
super.onClick(e, x, y);
this.notify('edge:click', this.getEventArgs(e, x, y));
}
onDblClick(e, x, y) {
super.onDblClick(e, x, y);
this.notify('edge:dblclick', this.getEventArgs(e, x, y));
}
onContextMenu(e, x, y) {
super.onContextMenu(e, x, y);
this.notify('edge:contextmenu', this.getEventArgs(e, x, y));
}
onMouseDown(e, x, y) {
this.notifyMouseDown(e, x, y);
this.startEdgeDragging(e, x, y);
}
onMouseMove(e, x, y) {
const data = this.getEventData(e);
switch (data.action) {
case 'drag-label': {
this.dragLabel(e, x, y);
break;
}
case 'drag-arrowhead': {
this.dragArrowhead(e, x, y);
break;
}
case 'drag-edge': {
this.dragEdge(e, x, y);
break;
}
default:
break;
}
this.notifyMouseMove(e, x, y);
return data;
}
onMouseUp(e, x, y) {
const data = this.getEventData(e);
switch (data.action) {
case 'drag-label': {
this.stopLabelDragging(e, x, y);
break;
}
case 'drag-arrowhead': {
this.stopArrowheadDragging(e, x, y);
break;
}
case 'drag-edge': {
this.stopEdgeDragging(e, x, y);
break;
}
default:
break;
}
this.notifyMouseUp(e, x, y);
this.checkMouseleave(e);
return data;
}
onMouseOver(e) {
super.onMouseOver(e);
this.notify('edge:mouseover', this.getEventArgs(e));
}
onMouseOut(e) {
super.onMouseOut(e);
this.notify('edge:mouseout', this.getEventArgs(e));
}
onMouseEnter(e) {
super.onMouseEnter(e);
this.notify('edge:mouseenter', this.getEventArgs(e));
}
onMouseLeave(e) {
super.onMouseLeave(e);
this.notify('edge:mouseleave', this.getEventArgs(e));
}
onMouseWheel(e, x, y, delta) {
super.onMouseWheel(e, x, y, delta);
this.notify('edge:mousewheel', Object.assign({ delta }, this.getEventArgs(e, x, y)));
}
onCustomEvent(e, name, x, y) {
// For default edge tool
const tool = Dom.findParentByClass(e.target, 'edge-tool', this.container);
if (tool) {
e.stopPropagation(); // no further action to be executed
if (this.can('useEdgeTools')) {
if (name === 'edge:remove') {
this.cell.remove({ ui: true });
return;
}
this.notify('edge:customevent', Object.assign({ name }, this.getEventArgs(e, x, y)));
}
this.notifyMouseDown(e, x, y);
}
else {
this.notify('edge:customevent', Object.assign({ name }, this.getEventArgs(e, x, y)));
super.onCustomEvent(e, name, x, y);
}
}
onLabelMouseDown(e, x, y) {
this.notifyMouseDown(e, x, y);
this.startLabelDragging(e, x, y);
const stopPropagation = this.getEventData(e).stopPropagation;
if (stopPropagation) {
e.stopPropagation();
}
}
// #region drag edge
startEdgeDragging(e, x, y) {
if (!this.can('edgeMovable')) {
this.notifyUnhandledMouseDown(e, x, y);
return;
}
this.setEventData(e, {
x,
y,
moving: false,
action: 'drag-edge',
});
}
dragEdge(e, x, y) {
const data = this.getEventData(e);
if (!data.moving) {
data.moving = true;
this.addClass('edge-moving');
this.notify('edge:move', {
e,
x,
y,
view: this,
cell: this.cell,
edge: this.cell,
});
}
this.cell.translate(x - data.x, y - data.y, { ui: true });
this.setEventData(e, { x, y });
this.notify('edge:moving', {
e,
x,
y,
view: this,
cell: this.cell,
edge: this.cell,
});
}
stopEdgeDragging(e, x, y) {
const data = this.getEventData(e);
if (data.moving) {
this.removeClass('edge-moving');
this.notify('edge:moved', {
e,
x,
y,
view: this,
cell: this.cell,
edge: this.cell,
});
}
data.moving = false;
}
// #endregion
// #region drag arrowhead
prepareArrowheadDragging(type, options) {
const magnet = this.getTerminalMagnet(type);
const data = {
action: 'drag-arrowhead',
x: options.x,
y: options.y,
isNewEdge: options.isNewEdge === true,
terminalType: type,
initialMagnet: magnet,
initialTerminal: ObjectExt.clone(this.cell[type]),
fallbackAction: options.fallbackAction || 'revert',
getValidateConnectionArgs: this.createValidateConnectionArgs(type),
options: options.options,
};
this.beforeArrowheadDragging(data);
return data;
}
createValidateConnectionArgs(type) {
const args = [
undefined,
undefined,
undefined,
undefined,
type,
this,
];
let opposite;
let i = 0;
let j = 0;
if (type === 'source') {
i = 2;
opposite = 'target';
}
else {
j = 2;
opposite = 'source';
}
const terminal = this.cell[opposite];
const cellId = terminal.cell;
if (cellId) {
let magnet;
const view = this.graph.findViewByCell(cellId);
args[i] = view;
if (view) {
magnet = view.getMagnetFromEdgeTerminal(terminal);
if (magnet === view.container) {
magnet = undefined;
}
}
args[i + 1] = magnet;
}
return (cellView, magnet) => {
args[j] = cellView;
args[j + 1] = cellView.container === magnet ? undefined : magnet;
return args;
};
}
beforeArrowheadDragging(data) {
data.zIndex = this.cell.zIndex;
this.cell.toFront();
const style = this.container.style;
data.pointerEvents = style.pointerEvents;
style.pointerEvents = 'none';
if (this.graph.options.connecting.highlight) {
this.highlightAvailableMagnets(data);
}
}
afterArrowheadDragging(data) {
if (data.zIndex != null) {
this.cell.setZIndex(data.zIndex, { ui: true });
data.zIndex = null;
}
const container = this.container;
container.style.pointerEvents = data.pointerEvents || '';
if (this.graph.options.connecting.highlight) {
this.unhighlightAvailableMagnets(data);
}
}
validateConnection(sourceView, sourceMagnet, targetView, targetMagnet, terminalType, edgeView, candidateTerminal) {
const options = this.graph.options.connecting;
const allowLoop = options.allowLoop;
const allowNode = options.allowNode;
const allowEdge = options.allowEdge;
const allowPort = options.allowPort;
const allowMulti = options.allowMulti;
const validate = options.validateConnection;
const edge = edgeView ? edgeView.cell : null;
const terminalView = terminalType === 'target' ? targetView : sourceView;
const terminalMagnet = terminalType === 'target' ? targetMagnet : sourceMagnet;
let valid = true;
const doValidate = (validate) => {
const sourcePort = terminalType === 'source'
? candidateTerminal
? candidateTerminal.port
: null
: edge
? edge.getSourcePortId()
: null;
const targetPort = terminalType === 'target'
? candidateTerminal
? candidateTerminal.port
: null
: edge
? edge.getTargetPortId()
: null;
return FunctionExt.call(validate, this.graph, {
edge,
edgeView,
sourceView,
targetView,
sourcePort,
targetPort,
sourceMagnet,
targetMagnet,
sourceCell: sourceView ? sourceView.cell : null,
targetCell: targetView ? targetView.cell : null,
type: terminalType,
});
};
if (allowLoop != null && sourceView != null && sourceView === targetView) {
if (typeof allowLoop === 'boolean') {
if (!allowLoop) {
valid = false;
}
}
else {
valid = doValidate(allowLoop);
}
}
if (valid && allowPor