@sapui5/sap.suite.ui.commons 1.136.11 → 1.136.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/sap/suite/ui/commons/.library +1 -1
- package/src/sap/suite/ui/commons/AriaProperties.js +1 -1
- package/src/sap/suite/ui/commons/CalculationBuilder.js +1 -1
- package/src/sap/suite/ui/commons/CalculationBuilderExpression.js +1 -1
- package/src/sap/suite/ui/commons/CalculationBuilderFunction.js +1 -1
- package/src/sap/suite/ui/commons/CalculationBuilderGroup.js +1 -1
- package/src/sap/suite/ui/commons/CalculationBuilderItem.js +1 -1
- package/src/sap/suite/ui/commons/CalculationBuilderValidationResult.js +1 -1
- package/src/sap/suite/ui/commons/CalculationBuilderVariable.js +1 -1
- package/src/sap/suite/ui/commons/CloudFilePicker.js +1 -1
- package/src/sap/suite/ui/commons/MicroProcessFlow.js +1 -1
- package/src/sap/suite/ui/commons/MicroProcessFlowItem.js +1 -1
- package/src/sap/suite/ui/commons/TimelineRenderManager.js +8 -4
- package/src/sap/suite/ui/commons/flexibility/changeHandler/PropertyChangeMapper.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/CropCustomShapeHistoryItem.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/CropEllipseHistoryItem.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/CropRectangleHistoryItem.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/CustomSizeItem.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/FilterHistoryItem.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/FlipHistoryItem.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/HistoryItem.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/ImageEditor.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/ImageEditorContainer.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/ImageEditorResponsiveContainer.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/ResizeHistoryItem.js +1 -1
- package/src/sap/suite/ui/commons/imageeditor/RotateHistoryItem.js +1 -1
- package/src/sap/suite/ui/commons/library.js +100 -3
- package/src/sap/suite/ui/commons/messagebundle.properties +73 -12
- package/src/sap/suite/ui/commons/messagebundle_ar.properties +1 -1
- package/src/sap/suite/ui/commons/messagebundle_en_US_saprigi.properties +39 -5
- package/src/sap/suite/ui/commons/messagebundle_fr_CA.properties +1 -1
- package/src/sap/suite/ui/commons/messagebundle_hu.properties +1 -1
- package/src/sap/suite/ui/commons/messagebundle_id.properties +2 -2
- package/src/sap/suite/ui/commons/messagebundle_it.properties +1 -1
- package/src/sap/suite/ui/commons/messagebundle_pt.properties +1 -1
- package/src/sap/suite/ui/commons/messagebundle_ru.properties +1 -1
- package/src/sap/suite/ui/commons/networkgraph/ElementBase.js +19 -1
- package/src/sap/suite/ui/commons/networkgraph/Graph.js +590 -48
- package/src/sap/suite/ui/commons/networkgraph/GraphMap.js +25 -3
- package/src/sap/suite/ui/commons/networkgraph/GraphRenderer.js +19 -8
- package/src/sap/suite/ui/commons/networkgraph/KeyboardNavigator.js +367 -12
- package/src/sap/suite/ui/commons/networkgraph/Line.js +814 -22
- package/src/sap/suite/ui/commons/networkgraph/Node.js +573 -79
- package/src/sap/suite/ui/commons/networkgraph/Tooltip.js +4 -0
- package/src/sap/suite/ui/commons/networkgraph/Utils.js +249 -10
- package/src/sap/suite/ui/commons/networkgraph/layout/NoopLayout.js +77 -7
- package/src/sap/suite/ui/commons/networkgraph/util/ConnectionPathUtils.js +1174 -0
- package/src/sap/suite/ui/commons/networkgraph/util/CreateConnectionPopover.js +374 -0
- package/src/sap/suite/ui/commons/networkgraph/util/DependencyLayoutHelper.js +1017 -0
- package/src/sap/suite/ui/commons/networkgraph/util/DragDropManager.js +721 -0
- package/src/sap/suite/ui/commons/networkgraph/util/PortManager.js +582 -0
- package/src/sap/suite/ui/commons/statusindicator/Circle.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/CustomShape.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/DiscreteThreshold.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/FillingOption.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/LibraryShape.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/Path.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/PropertyThreshold.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/Rectangle.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/Shape.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/ShapeGroup.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/SimpleShape.js +1 -1
- package/src/sap/suite/ui/commons/statusindicator/StatusIndicator.js +1 -1
- package/src/sap/suite/ui/commons/taccount/TAccount.js +1 -1
- package/src/sap/suite/ui/commons/taccount/TAccountGroup.js +1 -1
- package/src/sap/suite/ui/commons/taccount/TAccountItem.js +1 -1
- package/src/sap/suite/ui/commons/taccount/TAccountItemProperty.js +1 -1
- package/src/sap/suite/ui/commons/taccount/TAccountPanel.js +1 -1
- package/src/sap/suite/ui/commons/themes/base/NetworkGraph.less +26 -13
- package/src/sap/suite/ui/commons/themes/base/NetworkGroup.less +4 -0
- package/src/sap/suite/ui/commons/themes/base/NetworkLine.less +58 -13
- package/src/sap/suite/ui/commons/themes/base/NetworkNode.less +251 -47
- package/src/sap/suite/ui/commons/themes/base/SemanticColorMixins.less +55 -0
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
*
|
|
3
|
+
SAP UI development toolkit for HTML5 (SAPUI5)
|
|
4
|
+
(c) Copyright 2009-2015 SAP SE. All rights reserved
|
|
5
|
+
|
|
6
|
+
*/
|
|
7
|
+
sap.ui.define(["sap/suite/ui/commons/library", "sap/base/Log"], function (library, Log) {
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
const oConnectionType = library.networkgraph.ConnectionType,
|
|
11
|
+
oComponentArrangement = library.networkgraph.ComponentArrangement;
|
|
12
|
+
const DEFAULT_HORIZONTAL_SPACING = 240;
|
|
13
|
+
const DEFAULT_VERTICAL_SPACING = 100;
|
|
14
|
+
const DRAG_DETECTION_THRESHOLD = 10;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Utility class for calculating node positions based on dependency relationships.
|
|
18
|
+
* This utility arranges nodes based on their dependency relationships using topological sorting,
|
|
19
|
+
* cycle detection, and layer-based positioning.
|
|
20
|
+
*
|
|
21
|
+
* Why a node may be positioned incorrectly after recalculation:
|
|
22
|
+
* After each run, node positions are cached with a flag indicating whether the user moved
|
|
23
|
+
* them (<code>bIsUserPositioned</code>). On the next call, nodes flagged as user-positioned
|
|
24
|
+
* are preserved as-is, and the algorithm-placed nodes are recalculated. A node appears in the
|
|
25
|
+
* wrong place if it is unexpectedly treated as user-positioned, most commonly because
|
|
26
|
+
* <code>clearCache: true</code> was passed. It wipes the cache, but leaves the coordinates intact.
|
|
27
|
+
* So every node with non-zero coordinates is immediately re-classified as user-positioned
|
|
28
|
+
* and freezes, rather than being recalculated.
|
|
29
|
+
*
|
|
30
|
+
* Drag and drop recalculation - correct pattern:
|
|
31
|
+
* Call <code>resetNode(oNode)</code> for each dragged node, then call
|
|
32
|
+
* <code>calculatePositions</code> without <code>clearCache</code> (default <code>false</code>).
|
|
33
|
+
* <code>resetNode</code> evicts the node from the cache and zeros its coordinates, so only
|
|
34
|
+
* that node is repositioned; all the other nodes remain stable.
|
|
35
|
+
*
|
|
36
|
+
* Sentinel value:
|
|
37
|
+
* Coordinates <code>(0, 0)</code> mean "unpositioned". To place a node at the canvas origin,
|
|
38
|
+
* use <code>(0.1, 0.1)</code> or <code>(1, 1)</code> instead.
|
|
39
|
+
*
|
|
40
|
+
* @namespace sap.suite.ui.commons.networkgraph.util.DependencyLayoutHelper
|
|
41
|
+
* @public
|
|
42
|
+
* @since 1.136.19
|
|
43
|
+
*/
|
|
44
|
+
const DependencyLayoutHelper = {
|
|
45
|
+
/**
|
|
46
|
+
* WeakMap to store last calculated positions and origin flag for automatic drag detection.
|
|
47
|
+
* Structure: {x: number, y: number, bIsUserPositioned: boolean}
|
|
48
|
+
*/
|
|
49
|
+
_lastCalculated: new WeakMap(),
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Evicts one or more nodes from the position cache and zeros their coordinates so the next
|
|
53
|
+
* <code>calculatePositions</code> call repositions them while all other nodes remain stable.
|
|
54
|
+
* To understand the use for drag and drop recalculation, see the class-level JSDoc for the full pattern.
|
|
55
|
+
*
|
|
56
|
+
* @param {sap.suite.ui.commons.networkgraph.Node|sap.suite.ui.commons.networkgraph.Node[]} vNode - A single node or an array of nodes to reset
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
resetNode: function (vNode) {
|
|
60
|
+
const aNodes = Array.isArray(vNode) ? vNode : [vNode];
|
|
61
|
+
aNodes.forEach((oNode) => {
|
|
62
|
+
if (!oNode || !oNode.setX || !oNode.setY) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this._lastCalculated.delete(oNode);
|
|
66
|
+
oNode.setX(0);
|
|
67
|
+
oNode.setY(0);
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Calculates and sets positions for nodes without coordinates based on their dependency relationships.
|
|
73
|
+
* Uses topological sorting (Kahn's algorithm) and DFS-based cycle detection to arrange nodes
|
|
74
|
+
* in layers from left to right, ensuring no overlaps and minimal line crossings.
|
|
75
|
+
*
|
|
76
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The graph instance
|
|
77
|
+
* @param {object} [mConfig] - Configuration object
|
|
78
|
+
* @param {number} [mConfig.horizontalSpacing=240] - Horizontal spacing between layers
|
|
79
|
+
* @param {number} [mConfig.verticalSpacing=100] - Vertical spacing between nodes
|
|
80
|
+
* @param {sap.suite.ui.commons.networkgraph.ComponentArrangement} [mConfig.componentArrangement=Horizontal] - Component arrangement: "Horizontal" or "Vertical"
|
|
81
|
+
* @param {boolean} [mConfig.clearCache=false] - Wipes the position cache; node coordinates are not modified.
|
|
82
|
+
* <code>_detectUserChanges</code> then re-classifies every non-zero node as user-positioned and freezes it.
|
|
83
|
+
* Use only to discard all layout history (e.g. switching <code>componentArrangement</code>).
|
|
84
|
+
* For drag and drop recalculation, use <code>resetNode()</code> with the default <code>clearCache: false</code>.
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
calculatePositions: function (oGraph, mConfig) {
|
|
88
|
+
if (!oGraph) {
|
|
89
|
+
throw new Error("Graph instance is required");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const config = Object.assign(
|
|
93
|
+
{
|
|
94
|
+
horizontalSpacing: DEFAULT_HORIZONTAL_SPACING,
|
|
95
|
+
verticalSpacing: DEFAULT_VERTICAL_SPACING,
|
|
96
|
+
componentArrangement: oComponentArrangement.Horizontal,
|
|
97
|
+
clearCache: false
|
|
98
|
+
},
|
|
99
|
+
mConfig || {}
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Clear cache if requested (e.g., when changing arrangement)
|
|
103
|
+
if (config.clearCache) {
|
|
104
|
+
this._lastCalculated = new WeakMap();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
this._detectUserChanges(oGraph, config.horizontalSpacing);
|
|
109
|
+
this._buildDependencyGraph(oGraph);
|
|
110
|
+
const aComponents = this._identifyComponents(oGraph);
|
|
111
|
+
this._processComponents(aComponents, config);
|
|
112
|
+
this._calculateCoordinates(oGraph, config);
|
|
113
|
+
this._adjustHorizontalSpacingForNodeWidths(oGraph, config);
|
|
114
|
+
this._adjustVerticalSpacingForNodeHeights(oGraph, config);
|
|
115
|
+
if (oGraph._bIsRtl) {
|
|
116
|
+
this._horizontalMirror(oGraph, config.horizontalSpacing);
|
|
117
|
+
}
|
|
118
|
+
this._storeFinalPositions(oGraph, config.horizontalSpacing);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
Log.error("DependencyLayoutHelper error: " + e.message, e);
|
|
121
|
+
throw new Error("DependencyLayoutHelper error: " + e.message);
|
|
122
|
+
} finally {
|
|
123
|
+
if (this._lineMap) {
|
|
124
|
+
this._lineMap.clear();
|
|
125
|
+
this._lineMap = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Builds the dependency graph structure by analyzing node relationships.
|
|
132
|
+
* Initializes layout data for each node and categorizes connections.
|
|
133
|
+
*
|
|
134
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
|
|
135
|
+
* @private
|
|
136
|
+
*/
|
|
137
|
+
_buildDependencyGraph: function (oGraph) {
|
|
138
|
+
const aNodes = oGraph.getNodes();
|
|
139
|
+
const aLines = oGraph.getLines();
|
|
140
|
+
const mNodeMap = new Map();
|
|
141
|
+
|
|
142
|
+
aNodes.forEach((oNode) => {
|
|
143
|
+
mNodeMap.set(oNode.getKey(), oNode);
|
|
144
|
+
|
|
145
|
+
oNode._layoutData = {
|
|
146
|
+
layer: -1,
|
|
147
|
+
position: -1,
|
|
148
|
+
incoming: [],
|
|
149
|
+
outgoing: [],
|
|
150
|
+
parallelNodes: [],
|
|
151
|
+
backwardEdges: [],
|
|
152
|
+
backwardEdgeLines: [],
|
|
153
|
+
isBackwardSource: false
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (this._lineMap) {
|
|
158
|
+
this._lineMap.clear();
|
|
159
|
+
}
|
|
160
|
+
// Store line references for backward edge detection
|
|
161
|
+
this._lineMap = new Map();
|
|
162
|
+
|
|
163
|
+
aLines.forEach((oLine) => {
|
|
164
|
+
const oFromNode = mNodeMap.get(oLine.getFrom());
|
|
165
|
+
const oToNode = mNodeMap.get(oLine.getTo());
|
|
166
|
+
|
|
167
|
+
if (oFromNode && oToNode) {
|
|
168
|
+
const sConnectionType = oLine.getConnectionType();
|
|
169
|
+
const sFromKey = oFromNode.getKey();
|
|
170
|
+
const sToKey = oToNode.getKey();
|
|
171
|
+
|
|
172
|
+
if (!this._lineMap.has(sFromKey)) {
|
|
173
|
+
this._lineMap.set(sFromKey, new Map());
|
|
174
|
+
}
|
|
175
|
+
this._lineMap.get(sFromKey).set(sToKey, oLine);
|
|
176
|
+
|
|
177
|
+
switch (sConnectionType) {
|
|
178
|
+
case oConnectionType.RightToRight:
|
|
179
|
+
case oConnectionType.LeftToLeft:
|
|
180
|
+
oFromNode._layoutData.parallelNodes.push(oToNode);
|
|
181
|
+
oToNode._layoutData.parallelNodes.push(oFromNode);
|
|
182
|
+
break;
|
|
183
|
+
case oConnectionType.RightToLeft:
|
|
184
|
+
case oConnectionType.LeftToRight:
|
|
185
|
+
default:
|
|
186
|
+
oFromNode._layoutData.outgoing.push(oToNode);
|
|
187
|
+
oToNode._layoutData.incoming.push(oFromNode);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
this._detectAndRemoveBackwardEdges(aNodes);
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Detects and temporarily removes backward edges that create cycles using DFS.
|
|
198
|
+
* Identifies edges that point to already visited ancestors in the DFS traversal.
|
|
199
|
+
*
|
|
200
|
+
* @param {sap.suite.ui.commons.networkgraph.Node[]} aNodes - Array of graph nodes
|
|
201
|
+
* @private
|
|
202
|
+
*/
|
|
203
|
+
_detectAndRemoveBackwardEdges: function (aNodes) {
|
|
204
|
+
const mVisiting = {};
|
|
205
|
+
const mVisited = {};
|
|
206
|
+
|
|
207
|
+
const dfsVisit = (oNode) => {
|
|
208
|
+
const sKey = oNode.getKey();
|
|
209
|
+
if (mVisited[sKey]) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
mVisiting[sKey] = true;
|
|
214
|
+
|
|
215
|
+
const aRemainingOutgoing = [];
|
|
216
|
+
oNode._layoutData.outgoing.forEach((oTargetNode) => {
|
|
217
|
+
const sTargetKey = oTargetNode.getKey();
|
|
218
|
+
|
|
219
|
+
if (mVisiting[sTargetKey]) {
|
|
220
|
+
// Backward edge detected
|
|
221
|
+
oNode._layoutData.isBackwardSource = true;
|
|
222
|
+
oNode._layoutData.backwardEdges.push(oTargetNode);
|
|
223
|
+
|
|
224
|
+
const oLine = this._lineMap.get(sKey)?.get(sTargetKey);
|
|
225
|
+
if (oLine) {
|
|
226
|
+
oNode._layoutData.backwardEdgeLines.push(oLine);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const idx = oTargetNode._layoutData.incoming.indexOf(oNode);
|
|
230
|
+
if (idx > -1) {
|
|
231
|
+
oTargetNode._layoutData.incoming.splice(idx, 1);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
aRemainingOutgoing.push(oTargetNode);
|
|
235
|
+
if (!mVisited[sTargetKey]) {
|
|
236
|
+
dfsVisit(oTargetNode);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
oNode._layoutData.outgoing = aRemainingOutgoing;
|
|
242
|
+
mVisiting[sKey] = false;
|
|
243
|
+
mVisited[sKey] = true;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
aNodes.forEach((oNode) => {
|
|
247
|
+
if (!mVisited[oNode.getKey()]) {
|
|
248
|
+
dfsVisit(oNode);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Identifies separate connected components in the graph using BFS.
|
|
255
|
+
* A component is a group of nodes that are connected by dependencies.
|
|
256
|
+
*
|
|
257
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
|
|
258
|
+
* @returns {Array<sap.suite.ui.commons.networkgraph.Node[]>} Array of components (groups of connected nodes)
|
|
259
|
+
* @private
|
|
260
|
+
*/
|
|
261
|
+
_identifyComponents: function (oGraph) {
|
|
262
|
+
const aNodes = oGraph.getNodes();
|
|
263
|
+
const mVisited = {};
|
|
264
|
+
const aComponents = [];
|
|
265
|
+
|
|
266
|
+
const addConnectedNodesToQueue = (oNode, aQueue) => {
|
|
267
|
+
const aConnectionTypes = ["incoming", "outgoing", "parallelNodes"];
|
|
268
|
+
aConnectionTypes.forEach((sType) => {
|
|
269
|
+
oNode._layoutData[sType].forEach((oConnectedNode) => {
|
|
270
|
+
const sKey = oConnectedNode.getKey();
|
|
271
|
+
if (!mVisited[sKey]) {
|
|
272
|
+
aQueue.push(oConnectedNode);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
aNodes.forEach((oNode) => {
|
|
279
|
+
const sKey = oNode.getKey();
|
|
280
|
+
if (mVisited[sKey]) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const aComponent = [];
|
|
285
|
+
const aQueue = [oNode];
|
|
286
|
+
|
|
287
|
+
while (aQueue.length > 0) {
|
|
288
|
+
const oCurrent = aQueue.shift();
|
|
289
|
+
const sCurrentKey = oCurrent.getKey();
|
|
290
|
+
|
|
291
|
+
if (!mVisited[sCurrentKey]) {
|
|
292
|
+
mVisited[sCurrentKey] = true;
|
|
293
|
+
aComponent.push(oCurrent);
|
|
294
|
+
addConnectedNodesToQueue(oCurrent, aQueue);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
aComponents.push(aComponent);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
return aComponents;
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Processes all components: separates isolated nodes and arranges connected components.
|
|
306
|
+
*
|
|
307
|
+
* @param {Array<sap.suite.ui.commons.networkgraph.Node[]>} aComponents - Array of components (groups of connected nodes)
|
|
308
|
+
* @param {object} config - Configuration object
|
|
309
|
+
* @private
|
|
310
|
+
*/
|
|
311
|
+
_processComponents: function (aComponents, { componentArrangement }) {
|
|
312
|
+
const aIsolatedNodes = [];
|
|
313
|
+
const aConnectedComponents = [];
|
|
314
|
+
|
|
315
|
+
aComponents.forEach((aComponentNodes) => {
|
|
316
|
+
const bIsIsolated =
|
|
317
|
+
aComponentNodes.length === 1 &&
|
|
318
|
+
aComponentNodes[0]._layoutData.incoming.length === 0 &&
|
|
319
|
+
aComponentNodes[0]._layoutData.outgoing.length === 0;
|
|
320
|
+
|
|
321
|
+
if (bIsIsolated) {
|
|
322
|
+
aIsolatedNodes.push(aComponentNodes[0]);
|
|
323
|
+
} else {
|
|
324
|
+
aConnectedComponents.push(aComponentNodes);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
const bSideBySide = componentArrangement !== oComponentArrangement.Vertical;
|
|
328
|
+
let iMaxVerticalPosition = 0;
|
|
329
|
+
let iMaxLayerAcrossAll = 0;
|
|
330
|
+
|
|
331
|
+
aConnectedComponents.forEach((aComponentNodes, iCompIndex) => {
|
|
332
|
+
this._assignLayersForComponent(aComponentNodes, iCompIndex);
|
|
333
|
+
this._adjustParallelLayersForComponent(aComponentNodes);
|
|
334
|
+
this._assignVerticalPositionsForComponent(aComponentNodes);
|
|
335
|
+
|
|
336
|
+
if (bSideBySide) {
|
|
337
|
+
aComponentNodes.forEach((oNode) => {
|
|
338
|
+
oNode._layoutData.layer += iMaxLayerAcrossAll;
|
|
339
|
+
oNode._layoutData.componentOffset = 0;
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
let iComponentMaxLayer = -1;
|
|
343
|
+
aComponentNodes.forEach((n) => {
|
|
344
|
+
if (n._layoutData.layer > iComponentMaxLayer) {
|
|
345
|
+
iComponentMaxLayer = n._layoutData.layer;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
iMaxLayerAcrossAll = iComponentMaxLayer + 1;
|
|
349
|
+
} else {
|
|
350
|
+
aComponentNodes.forEach((oNode) => {
|
|
351
|
+
oNode._layoutData.position += iMaxVerticalPosition;
|
|
352
|
+
oNode._layoutData.componentOffset = 0;
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
let iComponentMaxPosition = -1;
|
|
356
|
+
aComponentNodes.forEach((n) => {
|
|
357
|
+
if (n._layoutData.position > iComponentMaxPosition) {
|
|
358
|
+
iComponentMaxPosition = n._layoutData.position;
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
iMaxVerticalPosition = iComponentMaxPosition + 1;
|
|
362
|
+
|
|
363
|
+
let iComponentMaxLayer = -1;
|
|
364
|
+
aComponentNodes.forEach((n) => {
|
|
365
|
+
if (n._layoutData.layer > iComponentMaxLayer) {
|
|
366
|
+
iComponentMaxLayer = n._layoutData.layer;
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
iMaxLayerAcrossAll = Math.max(iMaxLayerAcrossAll, iComponentMaxLayer);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
if (aIsolatedNodes.length > 0) {
|
|
374
|
+
const iRightSideOffset = bSideBySide ? iMaxLayerAcrossAll : iMaxLayerAcrossAll + 1;
|
|
375
|
+
let iIsolatedIdx = 0;
|
|
376
|
+
aIsolatedNodes.forEach((oNode) => {
|
|
377
|
+
if (this._lastCalculated.get(oNode)?.bIsUserPositioned) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const idx = iIsolatedIdx++;
|
|
381
|
+
|
|
382
|
+
oNode._layoutData.layer = Math.floor(idx / 2);
|
|
383
|
+
oNode._layoutData.position = idx % 2;
|
|
384
|
+
oNode._layoutData.componentOffset = iRightSideOffset;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Assigns layer numbers to nodes in a component (group of connected nodes) using Kahn's algorithm (topological sorting).
|
|
391
|
+
*
|
|
392
|
+
* @param {sap.suite.ui.commons.networkgraph.Node[]} aComponentNodes - Array of nodes belonging to the same component
|
|
393
|
+
* @param {number} iComponentId - Component identifier
|
|
394
|
+
* @private
|
|
395
|
+
*/
|
|
396
|
+
_assignLayersForComponent: function (aComponentNodes, iComponentId) {
|
|
397
|
+
const aQueue = [];
|
|
398
|
+
const mInDegree = {};
|
|
399
|
+
|
|
400
|
+
aComponentNodes.forEach((oNode) => {
|
|
401
|
+
oNode._layoutData.componentId = iComponentId;
|
|
402
|
+
const iInDegree = oNode._layoutData.incoming.length;
|
|
403
|
+
mInDegree[oNode.getKey()] = iInDegree;
|
|
404
|
+
|
|
405
|
+
if (iInDegree === 0) {
|
|
406
|
+
oNode._layoutData.layer = 0;
|
|
407
|
+
aQueue.push(oNode);
|
|
408
|
+
|
|
409
|
+
oNode._layoutData.parallelNodes.forEach((oParallelNode) => {
|
|
410
|
+
if (oParallelNode._layoutData.layer === -1) {
|
|
411
|
+
oParallelNode._layoutData.layer = 0;
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
while (aQueue.length > 0) {
|
|
418
|
+
const oCurrentNode = aQueue.shift();
|
|
419
|
+
|
|
420
|
+
oCurrentNode._layoutData.outgoing.forEach((oTargetNode) => {
|
|
421
|
+
const sTargetKey = oTargetNode.getKey();
|
|
422
|
+
mInDegree[sTargetKey]--;
|
|
423
|
+
|
|
424
|
+
oTargetNode._layoutData.layer = Math.max(
|
|
425
|
+
oTargetNode._layoutData.layer,
|
|
426
|
+
oCurrentNode._layoutData.layer + 1
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if (mInDegree[sTargetKey] === 0) {
|
|
430
|
+
aQueue.push(oTargetNode);
|
|
431
|
+
}
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const aUnprocessedNodes = aComponentNodes.filter(
|
|
436
|
+
(n) => n._layoutData.layer === -1
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
if (aUnprocessedNodes.length > 0) {
|
|
440
|
+
const aProcessedNodes = aComponentNodes.filter(
|
|
441
|
+
(n) => n._layoutData.layer >= 0
|
|
442
|
+
);
|
|
443
|
+
let iMinLayer = 0;
|
|
444
|
+
if (aProcessedNodes.length > 0) {
|
|
445
|
+
let iMaxLayer = -1;
|
|
446
|
+
aProcessedNodes.forEach((n) => {
|
|
447
|
+
if (n._layoutData.layer > iMaxLayer) {
|
|
448
|
+
iMaxLayer = n._layoutData.layer;
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
iMinLayer = iMaxLayer + 1;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
aUnprocessedNodes.forEach((oNode) => {
|
|
455
|
+
oNode._layoutData.layer = iMinLayer;
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
aComponentNodes.forEach((oNode) => {
|
|
460
|
+
if (
|
|
461
|
+
oNode._layoutData.incoming.length === 0 &&
|
|
462
|
+
oNode._layoutData.outgoing.length > 0
|
|
463
|
+
) {
|
|
464
|
+
const iCurrentLayer = oNode._layoutData.layer;
|
|
465
|
+
let iMaxTargetLayer = -1;
|
|
466
|
+
oNode._layoutData.outgoing.forEach((t) => {
|
|
467
|
+
if (t._layoutData.layer > iMaxTargetLayer) {
|
|
468
|
+
iMaxTargetLayer = t._layoutData.layer;
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const bTargetHasOtherIncoming = oNode._layoutData.outgoing.some(
|
|
473
|
+
(t) => t._layoutData.incoming.filter((n) => n !== oNode).length > 0
|
|
474
|
+
);
|
|
475
|
+
|
|
476
|
+
if (iMaxTargetLayer > iCurrentLayer && bTargetHasOtherIncoming) {
|
|
477
|
+
oNode._layoutData.layer = Math.max(0, iMaxTargetLayer - 1);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Ensures parallel nodes (same-layer dependencies) stay in the same layer.
|
|
485
|
+
*
|
|
486
|
+
* @param {sap.suite.ui.commons.networkgraph.Node[]} aComponentNodes - Array of nodes belonging to the same component (group of connected nodes)
|
|
487
|
+
* @private
|
|
488
|
+
*/
|
|
489
|
+
_adjustParallelLayersForComponent: function (aComponentNodes) {
|
|
490
|
+
const ensureParallelNodesInSameLayer = (oNode) => {
|
|
491
|
+
oNode._layoutData.parallelNodes.forEach((oParallelNode) => {
|
|
492
|
+
if (
|
|
493
|
+
oParallelNode._layoutData.layer === -1 ||
|
|
494
|
+
oParallelNode._layoutData.layer > oNode._layoutData.layer
|
|
495
|
+
) {
|
|
496
|
+
oParallelNode._layoutData.layer = oNode._layoutData.layer;
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
aComponentNodes.forEach((oNode) => {
|
|
502
|
+
if (oNode._layoutData.layer >= 0) {
|
|
503
|
+
ensureParallelNodesInSameLayer(oNode);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
let bChanged = true;
|
|
508
|
+
while (bChanged) {
|
|
509
|
+
bChanged = false;
|
|
510
|
+
|
|
511
|
+
aComponentNodes.forEach((oNode) => {
|
|
512
|
+
oNode._layoutData.parallelNodes.forEach((oParallelNode) => {
|
|
513
|
+
if (oNode._layoutData.layer !== oParallelNode._layoutData.layer) {
|
|
514
|
+
const iMinLayer = Math.min(
|
|
515
|
+
oNode._layoutData.layer,
|
|
516
|
+
oParallelNode._layoutData.layer
|
|
517
|
+
);
|
|
518
|
+
if (
|
|
519
|
+
oNode._layoutData.layer !== iMinLayer ||
|
|
520
|
+
oParallelNode._layoutData.layer !== iMinLayer
|
|
521
|
+
) {
|
|
522
|
+
oNode._layoutData.layer = iMinLayer;
|
|
523
|
+
oParallelNode._layoutData.layer = iMinLayer;
|
|
524
|
+
bChanged = true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Assigns vertical positions within layers, inheriting parent positions where possible.
|
|
534
|
+
*
|
|
535
|
+
* @param {sap.suite.ui.commons.networkgraph.Node[]} aComponentNodes - Array of nodes belonging to the same component (group of connected nodes)
|
|
536
|
+
* @private
|
|
537
|
+
*/
|
|
538
|
+
_assignVerticalPositionsForComponent: function (aComponentNodes) {
|
|
539
|
+
const mLayers = this._groupNodesByLayer(aComponentNodes);
|
|
540
|
+
const aLayerKeys = Object.keys(mLayers)
|
|
541
|
+
.map((k) => parseInt(k))
|
|
542
|
+
.sort((a, b) => a - b);
|
|
543
|
+
|
|
544
|
+
aLayerKeys.forEach((iLayer) => {
|
|
545
|
+
if (iLayer > 0) {
|
|
546
|
+
this._positionLayerNodes(mLayers, iLayer);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (mLayers[0]) {
|
|
551
|
+
const mUsedPositions = {};
|
|
552
|
+
let iNextAvailablePosition = 0;
|
|
553
|
+
|
|
554
|
+
const updateChainPositions = (
|
|
555
|
+
oNode,
|
|
556
|
+
iNewPosition,
|
|
557
|
+
mVisitedNodes = {}
|
|
558
|
+
) => {
|
|
559
|
+
if (mVisitedNodes[oNode.getKey()]) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
mVisitedNodes[oNode.getKey()] = true;
|
|
563
|
+
|
|
564
|
+
oNode._layoutData.position = iNewPosition;
|
|
565
|
+
|
|
566
|
+
oNode._layoutData.outgoing.forEach((oSuccessor) => {
|
|
567
|
+
const aSuccessorIncoming = oSuccessor._layoutData.incoming;
|
|
568
|
+
if (
|
|
569
|
+
aSuccessorIncoming.length === 1 &&
|
|
570
|
+
aSuccessorIncoming[0] === oNode
|
|
571
|
+
) {
|
|
572
|
+
updateChainPositions(oSuccessor, iNewPosition, mVisitedNodes);
|
|
573
|
+
}
|
|
574
|
+
});
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const aRootNodes = mLayers[0].sort((a, b) => {
|
|
578
|
+
const aHasTarget = a._layoutData.outgoing.length > 0;
|
|
579
|
+
const bHasTarget = b._layoutData.outgoing.length > 0;
|
|
580
|
+
|
|
581
|
+
if (!aHasTarget && !bHasTarget) return 0;
|
|
582
|
+
if (!aHasTarget) return 1;
|
|
583
|
+
if (!bHasTarget) return -1;
|
|
584
|
+
|
|
585
|
+
const aTargetPos = a._layoutData.outgoing[0]._layoutData.position;
|
|
586
|
+
const bTargetPos = b._layoutData.outgoing[0]._layoutData.position;
|
|
587
|
+
return aTargetPos - bTargetPos;
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
aRootNodes.forEach((oRootNode) => {
|
|
591
|
+
if (oRootNode._layoutData.outgoing.length > 0) {
|
|
592
|
+
const oFirstTarget = oRootNode._layoutData.outgoing[0];
|
|
593
|
+
const iPreferredPosition = oFirstTarget._layoutData.position;
|
|
594
|
+
|
|
595
|
+
if (!mUsedPositions[iPreferredPosition]) {
|
|
596
|
+
oRootNode._layoutData.position = iPreferredPosition;
|
|
597
|
+
mUsedPositions[iPreferredPosition] = true;
|
|
598
|
+
iNextAvailablePosition = Math.max(
|
|
599
|
+
iNextAvailablePosition,
|
|
600
|
+
iPreferredPosition + 1
|
|
601
|
+
);
|
|
602
|
+
} else {
|
|
603
|
+
const aTargetIncoming = oFirstTarget._layoutData.incoming;
|
|
604
|
+
if (
|
|
605
|
+
aTargetIncoming.length === 1 &&
|
|
606
|
+
aTargetIncoming[0] === oRootNode
|
|
607
|
+
) {
|
|
608
|
+
let iNewPosition = iPreferredPosition;
|
|
609
|
+
while (mUsedPositions[iNewPosition]) {
|
|
610
|
+
iNewPosition++;
|
|
611
|
+
}
|
|
612
|
+
updateChainPositions(oFirstTarget, iNewPosition);
|
|
613
|
+
oRootNode._layoutData.position = iNewPosition;
|
|
614
|
+
mUsedPositions[iNewPosition] = true;
|
|
615
|
+
iNextAvailablePosition = Math.max(
|
|
616
|
+
iNextAvailablePosition,
|
|
617
|
+
iNewPosition + 1
|
|
618
|
+
);
|
|
619
|
+
} else {
|
|
620
|
+
let iAdjacentPosition = iPreferredPosition + 1;
|
|
621
|
+
while (mUsedPositions[iAdjacentPosition]) {
|
|
622
|
+
iAdjacentPosition++;
|
|
623
|
+
}
|
|
624
|
+
oRootNode._layoutData.position = iAdjacentPosition;
|
|
625
|
+
mUsedPositions[iAdjacentPosition] = true;
|
|
626
|
+
iNextAvailablePosition = Math.max(
|
|
627
|
+
iNextAvailablePosition,
|
|
628
|
+
iAdjacentPosition + 1
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} else {
|
|
633
|
+
while (mUsedPositions[iNextAvailablePosition]) {
|
|
634
|
+
iNextAvailablePosition++;
|
|
635
|
+
}
|
|
636
|
+
oRootNode._layoutData.position = iNextAvailablePosition;
|
|
637
|
+
mUsedPositions[iNextAvailablePosition] = true;
|
|
638
|
+
iNextAvailablePosition++;
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Groups nodes by their assigned layer.
|
|
646
|
+
*
|
|
647
|
+
* @param {sap.suite.ui.commons.networkgraph.Node[]} aComponentNodes - Array of nodes belonging to the same component (group of connected nodes)
|
|
648
|
+
* @returns {object} Map of layer number to array of nodes
|
|
649
|
+
* @private
|
|
650
|
+
*/
|
|
651
|
+
_groupNodesByLayer: function (aComponentNodes) {
|
|
652
|
+
const mLayers = {};
|
|
653
|
+
|
|
654
|
+
aComponentNodes.forEach((oNode) => {
|
|
655
|
+
const iLayer = oNode._layoutData.layer;
|
|
656
|
+
if (!mLayers[iLayer]) {
|
|
657
|
+
mLayers[iLayer] = [];
|
|
658
|
+
}
|
|
659
|
+
mLayers[iLayer].push(oNode);
|
|
660
|
+
oNode._layoutData.position = -1;
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
return mLayers;
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Positions nodes within a specific layer based on their predecessors.
|
|
668
|
+
*
|
|
669
|
+
* @param {object} mLayers - Map of layer number to array of nodes
|
|
670
|
+
* @param {number} iLayer - The layer to position
|
|
671
|
+
* @private
|
|
672
|
+
*/
|
|
673
|
+
_positionLayerNodes: function (mLayers, iLayer) {
|
|
674
|
+
const aLayerNodes = mLayers[iLayer];
|
|
675
|
+
const mUsedPositions = {};
|
|
676
|
+
let iNextAvailablePosition = 0;
|
|
677
|
+
|
|
678
|
+
const aPrevLayerNodes = (mLayers[iLayer - 1] || [])
|
|
679
|
+
.filter((oNode) => oNode._layoutData.position >= 0)
|
|
680
|
+
.sort((a, b) => a._layoutData.position - b._layoutData.position);
|
|
681
|
+
|
|
682
|
+
aPrevLayerNodes.forEach((oPrevNode) => {
|
|
683
|
+
const aSuccessors = oPrevNode._layoutData.outgoing
|
|
684
|
+
.filter((oNode) => oNode._layoutData.layer === iLayer)
|
|
685
|
+
.sort((a, b) => a.getKey().localeCompare(b.getKey()));
|
|
686
|
+
|
|
687
|
+
if (aSuccessors.length === 0) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const iPreferredPosition = oPrevNode._layoutData.position;
|
|
692
|
+
if (
|
|
693
|
+
!mUsedPositions[iPreferredPosition] &&
|
|
694
|
+
aSuccessors[0]._layoutData.position === -1
|
|
695
|
+
) {
|
|
696
|
+
aSuccessors[0]._layoutData.position = iPreferredPosition;
|
|
697
|
+
mUsedPositions[iPreferredPosition] = true;
|
|
698
|
+
iNextAvailablePosition = Math.max(
|
|
699
|
+
iNextAvailablePosition,
|
|
700
|
+
iPreferredPosition + 1
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
aSuccessors.forEach((oNode) => {
|
|
705
|
+
if (oNode._layoutData.position === -1) {
|
|
706
|
+
while (mUsedPositions[iNextAvailablePosition]) {
|
|
707
|
+
iNextAvailablePosition++;
|
|
708
|
+
}
|
|
709
|
+
oNode._layoutData.position = iNextAvailablePosition;
|
|
710
|
+
mUsedPositions[iNextAvailablePosition] = true;
|
|
711
|
+
iNextAvailablePosition++;
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
aLayerNodes.forEach((oNode) => {
|
|
717
|
+
if (oNode._layoutData.position === -1) {
|
|
718
|
+
while (mUsedPositions[iNextAvailablePosition]) {
|
|
719
|
+
iNextAvailablePosition++;
|
|
720
|
+
}
|
|
721
|
+
oNode._layoutData.position = iNextAvailablePosition;
|
|
722
|
+
mUsedPositions[iNextAvailablePosition] = true;
|
|
723
|
+
iNextAvailablePosition++;
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Calculates final X and Y coordinates for all nodes based on layer and position.
|
|
730
|
+
*
|
|
731
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
|
|
732
|
+
* @param {object} config - Configuration with spacing values
|
|
733
|
+
* @private
|
|
734
|
+
*/
|
|
735
|
+
_calculateCoordinates: function (oGraph, { horizontalSpacing, verticalSpacing }) {
|
|
736
|
+
const aNodes = oGraph.getNodes();
|
|
737
|
+
const iHSpacing = horizontalSpacing;
|
|
738
|
+
const iVSpacing = verticalSpacing;
|
|
739
|
+
|
|
740
|
+
aNodes.forEach((oNode) => {
|
|
741
|
+
const iComponentOffset = oNode._layoutData.componentOffset || 0;
|
|
742
|
+
oNode.setX(
|
|
743
|
+
iHSpacing + (oNode._layoutData.layer + iComponentOffset) * iHSpacing
|
|
744
|
+
);
|
|
745
|
+
oNode.setY(iVSpacing + oNode._layoutData.position * iVSpacing);
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
aNodes.forEach((oNode) => {
|
|
749
|
+
const oLastPos = this._lastCalculated.get(oNode);
|
|
750
|
+
if (oLastPos?.bIsUserPositioned) {
|
|
751
|
+
oNode.setX(oLastPos.x);
|
|
752
|
+
oNode.setY(oLastPos.y);
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
},
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Adjusts horizontal spacing to accommodate nodes with varying widths.
|
|
759
|
+
*
|
|
760
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
|
|
761
|
+
* @private
|
|
762
|
+
*/
|
|
763
|
+
_adjustHorizontalSpacingForNodeWidths: function (oGraph) {
|
|
764
|
+
const aNodes = oGraph.getNodes();
|
|
765
|
+
const mLayerWidths = {};
|
|
766
|
+
let iMaxLayer = -1;
|
|
767
|
+
|
|
768
|
+
aNodes.forEach((oNode) => {
|
|
769
|
+
const iLayer =
|
|
770
|
+
oNode._layoutData.layer + (oNode._layoutData.componentOffset || 0);
|
|
771
|
+
// Skip nodes with unassigned layout
|
|
772
|
+
if (iLayer < 0) return;
|
|
773
|
+
iMaxLayer = Math.max(iMaxLayer, iLayer);
|
|
774
|
+
|
|
775
|
+
if (oNode._iWidth) {
|
|
776
|
+
const iWidth = oNode._iWidth;
|
|
777
|
+
if (!mLayerWidths[iLayer] || mLayerWidths[iLayer] < iWidth) {
|
|
778
|
+
mLayerWidths[iLayer] = iWidth;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
const mLayerOffsets = {};
|
|
784
|
+
let iCumulativeOffset = 0;
|
|
785
|
+
|
|
786
|
+
for (let iLayer = 0; iLayer <= iMaxLayer; iLayer++) {
|
|
787
|
+
mLayerOffsets[iLayer] = iCumulativeOffset;
|
|
788
|
+
const iActualWidth = mLayerWidths[iLayer];
|
|
789
|
+
if (iActualWidth) {
|
|
790
|
+
iCumulativeOffset += iActualWidth;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
aNodes.forEach((oNode) => {
|
|
795
|
+
if (!this._lastCalculated.get(oNode)?.bIsUserPositioned) {
|
|
796
|
+
const iLayer =
|
|
797
|
+
oNode._layoutData.layer + (oNode._layoutData.componentOffset || 0);
|
|
798
|
+
const iOffset = mLayerOffsets[iLayer] || 0;
|
|
799
|
+
oNode.setX(oNode.getX() + iOffset);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
},
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Adjusts vertical spacing to accommodate nodes with varying heights.
|
|
806
|
+
*
|
|
807
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
|
|
808
|
+
* @private
|
|
809
|
+
*/
|
|
810
|
+
_adjustVerticalSpacingForNodeHeights: function (oGraph) {
|
|
811
|
+
const aNodes = oGraph.getNodes();
|
|
812
|
+
const mPositionHeights = {};
|
|
813
|
+
const mAllPositions = {};
|
|
814
|
+
|
|
815
|
+
aNodes.forEach((oNode) => {
|
|
816
|
+
const iPosition = oNode._layoutData.position;
|
|
817
|
+
// Skip nodes with unassigned layout
|
|
818
|
+
if (iPosition < 0) return;
|
|
819
|
+
mAllPositions[iPosition] = true;
|
|
820
|
+
|
|
821
|
+
if (oNode._iHeight) {
|
|
822
|
+
const iHeight = oNode._iHeight;
|
|
823
|
+
if (
|
|
824
|
+
!mPositionHeights[iPosition] ||
|
|
825
|
+
mPositionHeights[iPosition] < iHeight
|
|
826
|
+
) {
|
|
827
|
+
mPositionHeights[iPosition] = iHeight;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
const aAllPositionKeys = Object.keys(mAllPositions)
|
|
833
|
+
.map((k) => parseInt(k))
|
|
834
|
+
.sort((a, b) => a - b);
|
|
835
|
+
const mPositionOffsets = {};
|
|
836
|
+
let iCumulativeOffset = 0;
|
|
837
|
+
|
|
838
|
+
aAllPositionKeys.forEach((iPosition) => {
|
|
839
|
+
mPositionOffsets[iPosition] = iCumulativeOffset;
|
|
840
|
+
const iActualHeight = mPositionHeights[iPosition];
|
|
841
|
+
if (iActualHeight) {
|
|
842
|
+
iCumulativeOffset += iActualHeight;
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
aNodes.forEach((oNode) => {
|
|
847
|
+
if (!this._lastCalculated.get(oNode)?.bIsUserPositioned) {
|
|
848
|
+
const iPosition = oNode._layoutData.position;
|
|
849
|
+
const iOffset = mPositionOffsets[iPosition] || 0;
|
|
850
|
+
oNode.setY(oNode.getY() + iOffset);
|
|
851
|
+
}
|
|
852
|
+
});
|
|
853
|
+
},
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Stores final calculated positions for automatic drag detection on next call.
|
|
857
|
+
* Only stores positions for nodes that were calculated (not original/dragged).
|
|
858
|
+
*
|
|
859
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
|
|
860
|
+
* @param {number} iHSpacing - Horizontal spacing used as RTL mirror offset
|
|
861
|
+
* @private
|
|
862
|
+
*/
|
|
863
|
+
_storeFinalPositions: function (oGraph, iHSpacing) {
|
|
864
|
+
const aNodes = oGraph.getNodes();
|
|
865
|
+
const bIsRtl = oGraph._bIsRtl || false;
|
|
866
|
+
|
|
867
|
+
// Reuse fMaxX from _horizontalMirror to ensure consistent mirror/unmirror
|
|
868
|
+
let fMaxX = 0;
|
|
869
|
+
if (bIsRtl) {
|
|
870
|
+
fMaxX = oGraph._rtlMirrorMaxX || 0;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
aNodes.forEach((oNode) => {
|
|
874
|
+
const fCurrentX = oNode.getX();
|
|
875
|
+
const fCurrentY = oNode.getY();
|
|
876
|
+
|
|
877
|
+
// Always store in LTR space
|
|
878
|
+
let fLtrX = fCurrentX;
|
|
879
|
+
if (bIsRtl) {
|
|
880
|
+
const fNodeWidth = oNode._iWidth || 0;
|
|
881
|
+
fLtrX = fMaxX - fCurrentX - fNodeWidth + iHSpacing;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const bIsUserPositioned = this._lastCalculated.get(oNode)?.bIsUserPositioned ?? false;
|
|
885
|
+
|
|
886
|
+
this._lastCalculated.set(oNode, {
|
|
887
|
+
x: fLtrX,
|
|
888
|
+
y: fCurrentY,
|
|
889
|
+
lastRtl: bIsRtl,
|
|
890
|
+
bIsUserPositioned
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
},
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Detects user-made position changes by comparing current positions with last calculated positions.
|
|
897
|
+
* Marks nodes with original coordinates and tracks drag operations.
|
|
898
|
+
*
|
|
899
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
|
|
900
|
+
* @param {number} iHSpacing - Horizontal spacing used as RTL mirror offset
|
|
901
|
+
* @private
|
|
902
|
+
*/
|
|
903
|
+
_detectUserChanges: function (oGraph, iHSpacing) {
|
|
904
|
+
const aNodes = oGraph.getNodes();
|
|
905
|
+
const bIsRtl = oGraph._bIsRtl || false;
|
|
906
|
+
|
|
907
|
+
// Use stored fMaxX from previous mirror operation if available, otherwise calculate.
|
|
908
|
+
// We need it both when in RTL mode AND when switching RTL→LTR (to detect pre-switch drags).
|
|
909
|
+
let fMaxX = oGraph._rtlMirrorMaxX || 0;
|
|
910
|
+
if (bIsRtl && !fMaxX) {
|
|
911
|
+
aNodes.forEach((n) => {
|
|
912
|
+
const fRight = n.getX() + (n._iWidth || 0);
|
|
913
|
+
if (fRight > fMaxX) fMaxX = fRight;
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
aNodes.forEach((oNode) => {
|
|
918
|
+
const fCurrentX = oNode.getX();
|
|
919
|
+
const fCurrentY = oNode.getY();
|
|
920
|
+
// (0,0) is the sentinel for "unpositioned": resetNode() sets coordinates to (0,0)
|
|
921
|
+
// so the algorithm treats the node as if it were never placed.
|
|
922
|
+
// To legitimately position a node at the canvas origin, use (0.1, 0.1) or (1, 1).
|
|
923
|
+
const bHasCoordinates = fCurrentX !== undefined && fCurrentY !== undefined && (fCurrentX !== 0 || fCurrentY !== 0);
|
|
924
|
+
const oLastPos = this._lastCalculated.get(oNode);
|
|
925
|
+
|
|
926
|
+
// Convert current position to LTR coordinates for comparison
|
|
927
|
+
let fLtrX = fCurrentX;
|
|
928
|
+
if (bIsRtl) {
|
|
929
|
+
const fNodeWidth = oNode._iWidth || 0;
|
|
930
|
+
fLtrX = fMaxX - fCurrentX - fNodeWidth + iHSpacing;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (oLastPos && !bHasCoordinates) {
|
|
934
|
+
// Coordinates were cleared (sentinel value 0,0) — evict cache so the algorithm
|
|
935
|
+
// repositions this node on the next layout pass.
|
|
936
|
+
this._lastCalculated.delete(oNode);
|
|
937
|
+
} else if (!oLastPos && bHasCoordinates) {
|
|
938
|
+
// No cache entry + non-zero coords: node has model-provided position (first call).
|
|
939
|
+
// Treated as user-positioned → preserved. Use resetNode() to opt a node out.
|
|
940
|
+
this._lastCalculated.set(oNode, {
|
|
941
|
+
x: fLtrX,
|
|
942
|
+
y: fCurrentY,
|
|
943
|
+
lastRtl: bIsRtl,
|
|
944
|
+
bIsUserPositioned: true
|
|
945
|
+
});
|
|
946
|
+
} else if (oLastPos) {
|
|
947
|
+
const bModeChanged = (oLastPos.lastRtl !== bIsRtl);
|
|
948
|
+
|
|
949
|
+
if (bModeChanged) {
|
|
950
|
+
const bPrevRtl = oLastPos.lastRtl;
|
|
951
|
+
let fLtrXBeforeSwitch;
|
|
952
|
+
|
|
953
|
+
if (bPrevRtl) {
|
|
954
|
+
const fNodeWidth = oNode._iWidth || 0;
|
|
955
|
+
fLtrXBeforeSwitch = fMaxX - fCurrentX - fNodeWidth + iHSpacing;
|
|
956
|
+
} else {
|
|
957
|
+
fLtrXBeforeSwitch = fCurrentX;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const fDeltaX = Math.abs(fLtrXBeforeSwitch - oLastPos.x);
|
|
961
|
+
const fDeltaY = Math.abs(fCurrentY - oLastPos.y);
|
|
962
|
+
|
|
963
|
+
if (fDeltaX > DRAG_DETECTION_THRESHOLD || fDeltaY > DRAG_DETECTION_THRESHOLD) {
|
|
964
|
+
oLastPos.x = fLtrXBeforeSwitch;
|
|
965
|
+
oLastPos.y = fCurrentY;
|
|
966
|
+
oLastPos.bIsUserPositioned = true;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
oLastPos.lastRtl = bIsRtl;
|
|
970
|
+
} else {
|
|
971
|
+
const fDeltaX = Math.abs(fLtrX - oLastPos.x);
|
|
972
|
+
const fDeltaY = Math.abs(fCurrentY - oLastPos.y);
|
|
973
|
+
|
|
974
|
+
if (fDeltaX > DRAG_DETECTION_THRESHOLD || fDeltaY > DRAG_DETECTION_THRESHOLD) {
|
|
975
|
+
oLastPos.x = fLtrX;
|
|
976
|
+
oLastPos.y = fCurrentY;
|
|
977
|
+
oLastPos.bIsUserPositioned = true;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
},
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Mirrors all node X positions horizontally for RTL layout.
|
|
986
|
+
*
|
|
987
|
+
* @param {sap.suite.ui.commons.networkgraph.Graph} oGraph - The network graph instance
|
|
988
|
+
* @param {number} iHSpacing - Horizontal spacing used as left-side padding offset after mirroring
|
|
989
|
+
* @private
|
|
990
|
+
*/
|
|
991
|
+
_horizontalMirror: function (oGraph, iHSpacing) {
|
|
992
|
+
const aNodes = oGraph.getNodes();
|
|
993
|
+
|
|
994
|
+
// Find the rightmost edge of all nodes
|
|
995
|
+
let fMaxX = 0;
|
|
996
|
+
aNodes.forEach((oNode) => {
|
|
997
|
+
const fX = oNode.getX();
|
|
998
|
+
const fWidth = oNode._iWidth || 0;
|
|
999
|
+
const fRightEdge = fX + fWidth;
|
|
1000
|
+
if (fRightEdge > fMaxX) {
|
|
1001
|
+
fMaxX = fRightEdge;
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
|
|
1005
|
+
oGraph._rtlMirrorMaxX = fMaxX;
|
|
1006
|
+
|
|
1007
|
+
aNodes.forEach((oNode) => {
|
|
1008
|
+
const fCurrentX = oNode.getX();
|
|
1009
|
+
const fWidth = oNode._iWidth || 0;
|
|
1010
|
+
const fMirroredX = fMaxX - fCurrentX - fWidth + iHSpacing;
|
|
1011
|
+
oNode.setX(fMirroredX);
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
return DependencyLayoutHelper;
|
|
1017
|
+
});
|