@sapui5/sap.suite.ui.commons 1.136.12 → 1.136.15

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