UNPKG

@antv/x6

Version:

JavaScript diagramming library that uses SVG and HTML for rendering.

1,315 lines 77.6 kB
var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; import { Rectangle, Polyline, Point, Angle, Path, Line } from '../geometry'; import { StringExt, ObjectExt, NumberExt, FunctionExt, Dom, Vector, } from '../util'; import { Router, Connector, NodeAnchor, EdgeAnchor, ConnectionPoint, } from '../registry'; import { Edge } from '../model/edge'; import { Markup } from './markup'; import { CellView } from './cell'; export class EdgeView extends CellView { constructor() { super(...arguments); this.POINT_ROUNDING = 2; this.markerCache = {}; // #endregion // #endregion } get [Symbol.toStringTag]() { return EdgeView.toStringTag; } getContainerClassName() { return [super.getContainerClassName(), this.prefixClassName('edge')].join(' '); } get sourceBBox() { const sourceView = this.sourceView; if (!sourceView) { const sourceDef = this.cell.getSource(); return new Rectangle(sourceDef.x, sourceDef.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) { const targetDef = this.cell.getTarget(); return new Rectangle(targetDef.x, targetDef.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'); } const graph = this.graph; const sourceView = this.sourceView; const targetView = this.targetView; if (graph && ((sourceView && !graph.renderer.isViewMounted(sourceView)) || (targetView && !graph.renderer.isViewMounted(targetView)))) { // Wait for the sourceView and targetView to be rendered. return ref; } if (this.hasAction(ref, 'render')) { this.render(); ref = this.removeAction(ref, [ 'render', 'update', 'vertices', 'labels', 'tools', 'widget', ]); return ref; } ref = this.handleAction(ref, 'vertices', () => this.renderVertexMarkers()); ref = this.handleAction(ref, 'update', () => this.update(null, options)); ref = this.handleAction(ref, 'labels', () => this.onLabelsChange(options)); ref = this.handleAction(ref, 'tools', () => { this.renderTools(); this.updateToolsPosition(); }); ref = this.handleAction(ref, 'widget', () => this.renderExternalTools()); return ref; } onLabelsChange(options = {}) { // Note: this optimization works in async=false mode only 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; } render() { this.empty(); this.containers = {}; this.renderMarkup(); this.renderLabels(); this.update(); return this; } renderMarkup() { const markup = this.cell.markup; if (markup) { if (typeof markup === 'string') { return this.renderStringMarkup(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); } renderStringMarkup(markup) { const cache = this.containers; const children = Vector.createVectors(markup); // Cache children elements for quicker access. children.forEach((child) => { const className = child.attr('class'); if (className) { cache[StringExt.camelCase(className)] = child.node; } }); this.renderTools(); this.renderVertexMarkers(); this.renderArrowheadMarkers(); Dom.append(this.container, children.map((child) => child.node)); } renderLabels() { const edge = this.cell; const labels = edge.getLabels(); const count = labels.length; let container = this.containers.labels; this.labelCache = {}; this.labelSelectors = {}; if (count <= 0) { if (container && 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.containers.labels = 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; } 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') { // default markup fragment is not wrapped in `<g/>` // add a `<g/>` container 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.containers.labels) { 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, }); } } } mergeLabelAttrs(hasCustomMarkup, labelAttrs, defaultLabelAttrs) { if (labelAttrs === null) { return null; } if (labelAttrs === undefined) { if (defaultLabelAttrs === null) { return null; } if (defaultLabelAttrs === undefined) { return undefined; } if (hasCustomMarkup) { return defaultLabelAttrs; } return ObjectExt.merge({}, defaultLabelAttrs); } if (hasCustomMarkup) { return ObjectExt.merge({}, defaultLabelAttrs, labelAttrs); } } customizeLabels() { if (this.containers.labels) { 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]; this.graph.hook.onEdgeLabelRendered({ edge, label, container, selectors, }); } } } renderTools() { const container = this.containers.tools; if (container == null) { return this; } const markup = this.cell.toolMarkup; const $container = this.$(container).empty(); if (Markup.isStringMarkup(markup)) { let template = StringExt.template(markup); const tool = Vector.create(template()); $container.append(tool.node); this.toolCache = tool.node; // If `doubleTools` is enabled, we render copy of the tools on the // other side of the edge as well but only if the edge is longer // than `longLength`. if (this.options.doubleTools) { let tool2; const doubleToolMarkup = this.cell.doubleToolMarkup; if (Markup.isStringMarkup(doubleToolMarkup)) { template = StringExt.template(doubleToolMarkup); tool2 = Vector.create(template()); } else { tool2 = tool.clone(); } $container.append(tool2.node); this.tool2Cache = tool2.node; } } return this; } renderExternalTools() { const tools = this.cell.getTools(); this.addTools(tools); return this; } renderVertexMarkers() { const container = this.containers.vertices; if (container == null) { return this; } const markup = this.cell.vertexMarkup; const $container = this.$(container).empty(); if (Markup.isStringMarkup(markup)) { const template = StringExt.template(markup); this.cell.getVertices().forEach((vertex, index) => { $container.append(Vector.create(template(Object.assign({ index }, vertex))).node); }); } return this; } renderArrowheadMarkers() { const container = this.containers.arrowheads; if (container == null) { return this; } const markup = this.cell.arrowheadMarkup; const $container = this.$(container).empty(); if (Markup.isStringMarkup(markup)) { const template = StringExt.template(markup); const sourceArrowhead = Vector.create(template({ end: 'source' })).node; const targetArrowhead = Vector.create(template({ end: 'target' })).node; this.containers.sourceArrowhead = sourceArrowhead; this.containers.targetArrowhead = targetArrowhead; $container.append(sourceArrowhead, targetArrowhead); } return this; } // #endregion // #region updating update(partialAttrs, options = {}) { this.cleanCache(); this.updateConnection(options); const attrs = this.cell.getAttrs(); if (attrs != null) { this.updateAttrs(this.container, attrs, { attrs: partialAttrs === attrs ? null : partialAttrs, selectors: this.selectors, }); } this.updateConnectionPath(); this.updateLabelPositions(); this.updateToolsPosition(); this.updateArrowheadMarkers(); if (options.toolId == null) { this.renderExternalTools(); } else { 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; } updateConnectionPath() { const containers = this.containers; if (containers.connection) { const pathData = this.getConnectionPathData(); containers.connection.setAttribute('d', pathData); } if (containers.connectionWrap) { const pathData = this.getConnectionPathData(); containers.connectionWrap.setAttribute('d', pathData); } if (containers.sourceMarker && containers.targetMarker) { this.translateAndAutoOrientArrows(containers.sourceMarker, containers.targetMarker); } } 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. 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 = EdgeAnchor.registry.get(name); if (typeof fn !== 'function') { return EdgeAnchor.registry.onNotFound(name); } anchor = FunctionExt.call(fn, this, cellView, magnet, ref, config.args || {}, terminalType); } else { const fn = NodeAnchor.registry.get(name); if (typeof fn !== 'function') { return NodeAnchor.registry.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 || Router.presets.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 ? Router.registry.get(name) : Router.presets.normal; if (typeof fn !== 'function') { return Router.registry.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 = ConnectionPoint.registry.get(name); if (typeof fn !== 'function') { return ConnectionPoint.registry.onNotFound(name); } const connectionPoint = FunctionExt.call(fn, this, line, view, magnet, args || {}, endType); return connectionPoint ? connectionPoint.round(this.POINT_ROUNDING) : anchor; } updateMarkerAttr(type) { const attrs = this.cell.getAttrs(); const key = `.${type}-marker`; const partial = attrs && attrs[key]; if (partial) { this.updateAttrs(this.container, {}, { attrs: { [key]: partial }, selectors: this.selectors, }); } } 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]; const sourceMarkerElem = this.containers.sourceMarker; const targetMarkerElem = this.containers.targetMarker; const cache = this.markerCache; let sourceMarkerPoint; let targetMarkerPoint; // Move the source point by the width of the marker taking into // account its scale around x-axis. Note that scale is the only // transform that makes sense to be set in `.marker-source` // attributes object as all other transforms (translate/rotate) // will be replaced by the `translateAndAutoOrient()` function. if (sourceMarkerElem) { this.updateMarkerAttr('source'); // support marker connection point registry??? cache.sourceBBox = cache.sourceBBox || Dom.getBBox(sourceMarkerElem); if (cache.sourceBBox.width > 0) { const scale = Dom.scale(sourceMarkerElem); sourceMarkerPoint = sourcePoint .clone() .move(firstRoutePoint || targetPoint, cache.sourceBBox.width * scale.sx * -1); } } else { const strokeWidth = getLineWidth('source'); if (strokeWidth) { sourceMarkerPoint = sourcePoint .clone() .move(firstRoutePoint || targetPoint, -strokeWidth); } } if (targetMarkerElem) { this.updateMarkerAttr('target'); cache.targetBBox = cache.targetBBox || Dom.getBBox(targetMarkerElem); if (cache.targetBBox.width > 0) { const scale = Dom.scale(targetMarkerElem); targetMarkerPoint = targetPoint .clone() .move(lastRoutePoint || sourcePoint, cache.targetBBox.width * scale.sx * -1); } } else { const strokeWidth = getLineWidth('target'); if (strokeWidth) { targetMarkerPoint = targetPoint .clone() .move(lastRoutePoint || sourcePoint, -strokeWidth); } } // If there was no markup for the marker, use the connection point. cache.sourcePoint = sourceMarkerPoint || sourcePoint.clone(); cache.targetPoint = 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 = Connector.registry.get(name); if (typeof method !== 'function') { return Connector.registry.onNotFound(name); } fn = method; } else { fn = Connector.presets.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) { const cache = this.markerCache; if (cache.sourcePoint) { cache.sourcePoint.translate(tx, ty); } if (cache.targetPoint) { cache.targetPoint.translate(tx, ty); } this.sourcePoint.translate(tx, ty); this.targetPoint.translate(tx, ty); this.sourceAnchor.translate(tx, ty); this.targetAnchor.translate(tx, ty); } updateLabelPositions() { if (this.containers.labels == 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 labelPosition = this.normalizeLabelPosition(label.position); const pos = ObjectExt.merge({}, defaultPosition, labelPosition); const matrix = this.getLabelTransformationMatrix(pos); this.labelCache[i].setAttribute('transform', Dom.matrixToTransformString(matrix)); } return this; } updateToolsPosition() { if (this.containers.tools == null) { return this; } // Move the tools a bit to the target position but don't cover the // `sourceArrowhead` marker. Note that the offset is hardcoded here. // The offset should be always more than the // `this.$('.marker-arrowhead[end="source"]')[0].bbox().width` but looking // this up all the time would be slow. let scale = ''; let offset = this.options.toolsOffset; const connectionLength = this.getConnectionLength(); // Firefox returns `connectionLength=NaN` in odd cases (for bezier curves). // In that case we won't update tools position at all. if (connectionLength != null) { // If the edge is too short, make the tools half the // size and the offset twice as low. if (connectionLength < this.options.shortLength) { scale = 'scale(.5)'; offset /= 2; } let pos = this.getPointAtLength(offset); if (pos != null) { Dom.attr(this.toolCache, 'transform', `translate(${pos.x},${pos.y}) ${scale}`); } if (this.options.doubleTools && connectionLength >= this.options.longLength) { const doubleToolsOffset = this.options.doubleToolsOffset || offset; pos = this.getPointAtLength(connectionLength - doubleToolsOffset); if (pos != null) { Dom.attr(this.tool2Cache, 'transform', `translate(${pos.x},${pos.y}) ${scale}`); } Dom.attr(this.tool2Cache, 'visibility', 'visible'); } else if (this.options.doubleTools) { Dom.attr(this.tool2Cache, 'visibility', 'hidden'); } } return this; } updateArrowheadMarkers() { const container = this.containers.arrowheads; if (container == null) { return this; } if (container.style.display === 'none') { return this; } const sourceArrowhead = this.containers.sourceArrowhead; const targetArrowhead = this.containers.targetArrowhead; if (sourceArrowhead && targetArrowhead) { const len = this.getConnectionLength() || 0; const sx = len < this.options.shortLength ? 0.5 : 1; Dom.scale(sourceArrowhead, sx); Dom.scale(targetArrowhead, sx); this.translateAndAutoOrientArrows(sourceArrowhead, targetArrowhead); } 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; } } translateAndAutoOrientArrows(sourceArrow, targetArrow) { const route = this.routePoints; if (sourceArrow) { Dom.translateAndAutoOrient(sourceArrow, this.sourcePoint, route[0] || this.targetPoint, this.graph.view.stage); } if (targetArrow) { Dom.translateAndAutoOrient(targetArrow, this.targetPoint, route[route.length - 1] || this.sourcePoint, this.graph.view.stage); } } 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; } } // merge default label position args into label position args // keep `undefined` or `null` because `{}` means something else mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs) { if (labelPositionArgs === null) { return null; } if (labelPositionArgs === undefined) { if (defaultLabelPositionArgs === null) { return null; } return defaultLabelPositionArgs; } return ObjectExt.merge({}, defaultLabelPositionArgs, labelPositionArgs); } addLabel(p1, p2, p3, options) { let localX; let localY; let localAngle = 0; let localOptions; if (typeof p1 !== 'number') { localX = p1.x; localY = p1.y; if (typeof p2 === 'number') { localAngle = p2; localOptions = p3; } else { localOptions = p2; } } else { localX = p1; localY = p2; if (typeof p3 === 'number') { localAngle = p3; localOptions = options; } else { localOptions = p3; } } // merge label position arguments const defaultLabelPositionArgs = this.getDefaultLabelPositionArgs(); const labelPositionArgs = localOptions; const positionArgs = this.mergeLabelPositionArgs(labelPositionArgs, defaultLabelPositionArgs); // append label to labels array const label = { position: this.getLabelPosition(localX, localY, localAngle, positionArgs), }; const index = -1; this.cell.insertLabel(label, index, localOptions); return index; } addVertex(x, y, options) { const isPoint = typeof x !== 'number'; const localX = isPoint ? x.x : x; const localY = isPoint ? x.y : y; const localOptions = isPoint ? y : options; const vertex = { x: localX, y: localY }; const index = this.getVertexIndex(localX, localY); this.cell.insertVertex(vertex, index, localOptions); return index; } sendToken(token, options, callback) { let duration; let reversed; let selector; let rorate; let timing = 'linear'; if (typeof options === 'object') { duration = options.duration; reversed = options.reversed === true; selector = options.selector; if (options.rotate === false) { rorate = ''; } else if (options.rotate === true) { rorate = 'auto'; } else if (options.rotate != null) { rorate = `${options.rotate}`; } if (options.timing) { timing = options.timing; } } else { duration = options; reversed = false; selector = null; } duration = duration || 1000; const attrs = { dur: `${duration}ms`, repeatCount: '1', calcMode: timing, fill: 'freeze', }; if (rorate) { attrs.rotate = rorate; } if (reversed) { attrs.keyPoints = '1;0'; attrs.keyTimes = '0;1'; } if (typeof options === 'object') { const { duration, reversed, selector, rotate, timing } = options, others = __rest(options, ["duration", "reversed", "selector", "rotate", "timing"]); Object.keys(others).forEach((key) => { attrs[key] = others[key]; }); } let path; if (typeof selector === 'string') { path = this.findOne(selector, this.container, this.selectors); } else { // Select connection path automatically. path = this.containers.connection ? this.containers.connection : this.container.querySelector('path'); } if (!(path instanceof SVGPathElement)) { throw new Error('Token animation requires a valid connection path.'); } const target = typeof token === 'string' ? this.findOne(token) : token; if (target == null) { throw new Error('Token animation requires a valid token element.'); } const parent = target.parentNode; const revert = () => { if (!parent) { Dom.remove(target); } }; const vToken = Vector.create(target); if (!parent) { vToken.appendTo(this.graph.view.stage); } const onComplete = attrs.complete; attrs.complete = (e) => { revert(); if (callback) { callback(); } if (onComplete) { onComplete(e); } }; const stop = vToken.animateAlongPath(attrs, path); return () => { revert(); stop(); }; } // #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) { 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 && options.absoluteOffset; const isDistanceRelative = !(options && options.absoluteDistance); const isDistanceAbsoluteReverse = options && 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 = path.closestPointT(labelPoint, pathOptions); // 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); const labelOffsetDiff = labelPoint.diff(closestPoint); labelOffset = { x: labelOffsetDiff.x, y: labelOffsetDiff.y }; } 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 = offs