@sapui5/sap.suite.ui.commons 1.136.12 → 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_en_US_saprigi.properties +39 -5
- package/src/sap/suite/ui/commons/messagebundle_fr_CA.properties +1 -1
- package/src/sap/suite/ui/commons/networkgraph/ElementBase.js +19 -1
- package/src/sap/suite/ui/commons/networkgraph/Graph.js +554 -45
- 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
|
@@ -21,12 +21,17 @@ sap.ui.define([
|
|
|
21
21
|
ArrowOrientation = library.networkgraph.LineArrowOrientation,
|
|
22
22
|
Shape = library.networkgraph.NodeShape,
|
|
23
23
|
Orientation = library.networkgraph.Orientation,
|
|
24
|
-
SemanticColorType = library.SemanticColorType
|
|
24
|
+
SemanticColorType = library.SemanticColorType,
|
|
25
|
+
ConnectionType = library.networkgraph.ConnectionType;
|
|
25
26
|
|
|
26
27
|
var BEND_RADIUS = 6, // Bezier 'radius' of smooth bends
|
|
27
28
|
FOCUS_LANE_WIDTH = 5, // Distance of focus shadow line from the main line
|
|
28
29
|
RELATIVE_ARROW_POSITION = 0.45,
|
|
29
30
|
FIXED_ARROW_POSITION = 15,
|
|
31
|
+
MULTIPLE_ARROW_DISTANCE = 44,
|
|
32
|
+
LABEL_BOX_WIDTH = 30,
|
|
33
|
+
LABEL_BOX_HEIGHT = 22,
|
|
34
|
+
ARROW_LENGTH = 11, // Length of arrow from apex to tail
|
|
30
35
|
ZERO_ANGLE_ARROW_POINTS = {
|
|
31
36
|
Apex: {x: 5.5, y: 0},
|
|
32
37
|
Second: {x: -5.5, y: -7.5},
|
|
@@ -114,6 +119,34 @@ sap.ui.define([
|
|
|
114
119
|
*/
|
|
115
120
|
stretchToCenter: {
|
|
116
121
|
type: "boolean", group: "Misc", defaultValue: false
|
|
122
|
+
},
|
|
123
|
+
/**
|
|
124
|
+
* Defines the connection type between the source and target nodes.
|
|
125
|
+
* Can be set to RightToLeft, LeftToRight, TopToBottom, or BottomToTop using {@link sap.suite.ui.commons.networkgraph.ConnectionType ConnectionType} enumeration.
|
|
126
|
+
* This property is used to determine how the line connects the source and target nodes. For example, if set to RightToLeft, the line starts from the right side of the source node and end at the left side of the target node.
|
|
127
|
+
* This property is valid only with the NoopLayout and drag and drop enabled network graph.
|
|
128
|
+
* @public
|
|
129
|
+
* @since 1.136.19
|
|
130
|
+
*
|
|
131
|
+
*/
|
|
132
|
+
connectionType: {
|
|
133
|
+
type: "sap.suite.ui.commons.networkgraph.ConnectionType", defaultValue: ConnectionType.RightToLeft
|
|
134
|
+
},
|
|
135
|
+
/**
|
|
136
|
+
* Defines the text label to be displayed on the line.
|
|
137
|
+
*
|
|
138
|
+
* When set, displays the label text in a rounded rectangle box positioned near the target arrow.
|
|
139
|
+
* The label is rendered only when all of the following conditions are met:
|
|
140
|
+
* - This property has a non-empty value
|
|
141
|
+
* - The graph is using NoopLayout
|
|
142
|
+
* - The line's arrowPosition is set to "Both"
|
|
143
|
+
* - The line is long enough to accommodate the label
|
|
144
|
+
* If this property is empty or not set, no label is displayed on the line
|
|
145
|
+
* @public
|
|
146
|
+
* @since 1.136.19
|
|
147
|
+
*/
|
|
148
|
+
labelName: {
|
|
149
|
+
type: "string", group: "Behavior", defaultValue: ""
|
|
117
150
|
}
|
|
118
151
|
},
|
|
119
152
|
aggregations: {
|
|
@@ -297,13 +330,57 @@ sap.ui.define([
|
|
|
297
330
|
"data-sap-ui": sId
|
|
298
331
|
}, false, oRm);
|
|
299
332
|
|
|
333
|
+
// Add title for tooltip showing connection type (always shown)
|
|
334
|
+
if (this.getParent()._isNoopLayout() &&
|
|
335
|
+
this.getArrowPosition() === ArrowPosition.Both) {
|
|
336
|
+
|
|
337
|
+
var sConnectionType = this.getConnectionType();
|
|
338
|
+
var oMapping = this.getParent().getConnectionTypeMapping();
|
|
339
|
+
var sTooltipText = "";
|
|
340
|
+
|
|
341
|
+
if (sConnectionType === ConnectionType.LeftToRight) {
|
|
342
|
+
sTooltipText = oMapping.LeftToRight || ConnectionType.LeftToRight;
|
|
343
|
+
} else if (sConnectionType === ConnectionType.RightToLeft) {
|
|
344
|
+
sTooltipText = oMapping.RightToLeft || ConnectionType.RightToLeft;
|
|
345
|
+
} else if (sConnectionType === ConnectionType.LeftToLeft) {
|
|
346
|
+
sTooltipText = oMapping.LeftToLeft || ConnectionType.LeftToLeft;
|
|
347
|
+
} else if (sConnectionType === ConnectionType.RightToRight) {
|
|
348
|
+
sTooltipText = oMapping.RightToRight || ConnectionType.RightToRight;
|
|
349
|
+
} else {
|
|
350
|
+
sTooltipText = oMapping.RightToLeft || ConnectionType.RightToLeft;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (sTooltipText) {
|
|
354
|
+
oRm.openStart("title");
|
|
355
|
+
oRm.openEnd();
|
|
356
|
+
oRm.text(sTooltipText);
|
|
357
|
+
oRm.close("title");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
300
361
|
// invisible wrapper for better event handling
|
|
301
362
|
fnRenderPath("sapSuiteUiCommonsNetworkLineInvisibleWrapper", "invisibleWrapper", true);
|
|
302
363
|
|
|
303
364
|
// path itself
|
|
304
|
-
|
|
365
|
+
fnRenderPath("", "path");
|
|
366
|
+
|
|
367
|
+
if (this.getArrowPosition() === ArrowPosition.Both && this._hasMultipleDirectedArrows() && this.getCoordinates().length >= 2 ) {
|
|
368
|
+
// Calculate total line length to determine arrow placement strategy
|
|
369
|
+
var fTotalLineLength = this._calculateTotalLineLength();
|
|
370
|
+
var bCanFitTwoArrows = this._canLineFitTwoArrows(fTotalLineLength);
|
|
305
371
|
|
|
306
|
-
|
|
372
|
+
if (bCanFitTwoArrows) {
|
|
373
|
+
// Line is long enough for both arrows - use smart source arrow logic
|
|
374
|
+
if (this._shouldRenderSourceArrow()) {
|
|
375
|
+
fnRenderArrow(ArrowOrientation.ParentOf, "multipleSource", "arrow-source");
|
|
376
|
+
}
|
|
377
|
+
// Always render target arrow (each incoming line gets its own target arrow)
|
|
378
|
+
fnRenderArrow(ArrowOrientation.ParentOf, "multipleTarget", "arrow-target");
|
|
379
|
+
} else {
|
|
380
|
+
// Line is too short for two arrows - only render target arrow
|
|
381
|
+
fnRenderArrow(ArrowOrientation.ParentOf, "multipleTarget", "arrow-target");
|
|
382
|
+
}
|
|
383
|
+
} else if (this.getArrowOrientation() !== ArrowOrientation.None && this.getCoordinates().length >= 2) {
|
|
307
384
|
if (this.getArrowOrientation() === ArrowOrientation.Both) {
|
|
308
385
|
if (this.getArrowPosition() === ArrowPosition.Middle) {
|
|
309
386
|
// middle arrow is rendered "at once" using two groups of points
|
|
@@ -336,6 +413,169 @@ sap.ui.define([
|
|
|
336
413
|
oRm.close("g");
|
|
337
414
|
};
|
|
338
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Renders the text label for this line. Used by batch text rendering.
|
|
418
|
+
* @param {sap.ui.core.RenderManager} oRm The RenderManager to use for rendering
|
|
419
|
+
* @param {string} [sIdSuffix] Optional suffix for element ID
|
|
420
|
+
* @private
|
|
421
|
+
*/
|
|
422
|
+
Line.prototype._renderLineText = function (oRm, sIdSuffix) {
|
|
423
|
+
// Early return if any condition is not met:
|
|
424
|
+
// - labelName has a non-empty value
|
|
425
|
+
// - Graph must be in NoopLayout
|
|
426
|
+
// - arrowPosition must be Both (labels only shown when both arrows are present)
|
|
427
|
+
if (!this.getLabelName() ||
|
|
428
|
+
!this.getParent()._isNoopLayout() ||
|
|
429
|
+
this.getArrowPosition() !== ArrowPosition.Both) {
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Always use the base line ID without any suffix to ensure consistency
|
|
434
|
+
// with _hideShowLineText which can't know what suffix was used during rendering
|
|
435
|
+
var sId = this.getId();
|
|
436
|
+
var {style: sStyle, class: sStatusClass} = this._getStatusStyle({
|
|
437
|
+
"stroke": ElementBase.ColorType.Border,
|
|
438
|
+
"stroke-width": ElementBase.ColorType.BorderWidth,
|
|
439
|
+
"stroke-dasharray": ElementBase.ColorType.BorderStyle
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
// Get the connection type and convert to display text
|
|
443
|
+
var sConnectionType = this.getConnectionType();
|
|
444
|
+
var oMapping = this.getParent().getConnectionTypeMapping();
|
|
445
|
+
var sFullText = ""; // Store the full text for tooltip
|
|
446
|
+
|
|
447
|
+
// Check if custom mapping exists for this connection type
|
|
448
|
+
switch (sConnectionType) {
|
|
449
|
+
case ConnectionType.LeftToRight:
|
|
450
|
+
sFullText = oMapping.LeftToRight || ConnectionType.LeftToRight;
|
|
451
|
+
break;
|
|
452
|
+
case ConnectionType.RightToLeft:
|
|
453
|
+
sFullText = oMapping.RightToLeft || ConnectionType.RightToLeft;
|
|
454
|
+
break;
|
|
455
|
+
case ConnectionType.LeftToLeft:
|
|
456
|
+
sFullText = oMapping.LeftToLeft || ConnectionType.LeftToLeft;
|
|
457
|
+
break;
|
|
458
|
+
case ConnectionType.RightToRight:
|
|
459
|
+
sFullText = oMapping.RightToRight || ConnectionType.RightToRight;
|
|
460
|
+
break;
|
|
461
|
+
default:
|
|
462
|
+
sFullText = oMapping.RightToLeft || ConnectionType.RightToLeft;
|
|
463
|
+
break;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Calculate position near the ingoing arrow (end of the line)
|
|
467
|
+
// When ArrowPosition.Both, the target arrow is positioned at MULTIPLE_ARROW_DISTANCE (44px) from the end
|
|
468
|
+
var oCoords = this.getCoordinates();
|
|
469
|
+
if (oCoords && oCoords.length >= 2) {
|
|
470
|
+
var aFragmentLengthSum = [], fTotalDistance = 0;
|
|
471
|
+
var iLastIndex = oCoords.length - 1;
|
|
472
|
+
|
|
473
|
+
// Calculate cumulative distances along the path
|
|
474
|
+
for (var i = 0; i < iLastIndex; i++) {
|
|
475
|
+
if (oCoords[i].getX() === oCoords[i + 1].getX()) {
|
|
476
|
+
// Vertical line - only Y changes
|
|
477
|
+
fTotalDistance += Math.abs(oCoords[i + 1].getY() - oCoords[i].getY());
|
|
478
|
+
} else if (oCoords[i].getY() === oCoords[i + 1].getY()) {
|
|
479
|
+
// Horizontal line - only X changes
|
|
480
|
+
fTotalDistance += Math.abs(oCoords[i + 1].getX() - oCoords[i].getX());
|
|
481
|
+
} else {
|
|
482
|
+
// Diagonal line - use Pythagorean theorem If Lines are plotted Diagonally
|
|
483
|
+
fTotalDistance += Math.sqrt(
|
|
484
|
+
Math.pow(oCoords[i + 1].getX() - oCoords[i].getX(), 2) +
|
|
485
|
+
Math.pow(oCoords[i + 1].getY() - oCoords[i].getY(), 2)
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
// Store cumulative distance
|
|
489
|
+
aFragmentLengthSum.push(fTotalDistance);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Check if there's enough space for the label box with 8px padding from target arrow
|
|
493
|
+
// When line is long enough for both arrows, check gap between them
|
|
494
|
+
// When line is short, source arrow is hidden and target arrow position is adjusted
|
|
495
|
+
var bCanFitTwoArrows = this._canLineFitTwoArrows(fTotalDistance);
|
|
496
|
+
var fTargetArrowDistance;
|
|
497
|
+
var fAvailableSpace;
|
|
498
|
+
|
|
499
|
+
if (bCanFitTwoArrows) {
|
|
500
|
+
// Both arrows present at fixed positions
|
|
501
|
+
fTargetArrowDistance = MULTIPLE_ARROW_DISTANCE;
|
|
502
|
+
// Available space = total - (source position + source arrow + target position + target arrow + 8px padding)
|
|
503
|
+
fAvailableSpace = fTotalDistance - (MULTIPLE_ARROW_DISTANCE + ARROW_LENGTH + MULTIPLE_ARROW_DISTANCE + ARROW_LENGTH + 8);
|
|
504
|
+
} else {
|
|
505
|
+
// Only target arrow present
|
|
506
|
+
fTargetArrowDistance = Math.min(MULTIPLE_ARROW_DISTANCE, fTotalDistance * 0.3);
|
|
507
|
+
// Available space = total - (target position + target arrow + 8px padding)
|
|
508
|
+
fAvailableSpace = fTotalDistance - (fTargetArrowDistance + ARROW_LENGTH + 8);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
var fRequiredSpace = LABEL_BOX_WIDTH; // 30px box width
|
|
512
|
+
if (fAvailableSpace < fRequiredSpace) {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Position label so its end is 8px away from the target arrow
|
|
517
|
+
var fTargetDistance = fTotalDistance - fTargetArrowDistance - 8 - LABEL_BOX_WIDTH; // arrow position + 8px offset + full box width
|
|
518
|
+
var iTargetIndex = 0;
|
|
519
|
+
for (var j = 0; j < iLastIndex; j++) {
|
|
520
|
+
if (aFragmentLengthSum[j] >= fTargetDistance) {
|
|
521
|
+
iTargetIndex = j;
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Calculate the exact position on the segment
|
|
527
|
+
var fSegmentStart = iTargetIndex > 0 ? aFragmentLengthSum[iTargetIndex - 1] : 0;
|
|
528
|
+
var fSegmentLength = aFragmentLengthSum[iTargetIndex] - fSegmentStart;
|
|
529
|
+
var fPositionInSegment = (fTargetDistance - fSegmentStart) / fSegmentLength;
|
|
530
|
+
var fLabelX = oCoords[iTargetIndex].getX() +
|
|
531
|
+
(oCoords[iTargetIndex + 1].getX() - oCoords[iTargetIndex].getX()) * fPositionInSegment;
|
|
532
|
+
var fLabelY = oCoords[iTargetIndex].getY() +
|
|
533
|
+
(oCoords[iTargetIndex + 1].getY() - oCoords[iTargetIndex].getY()) * fPositionInSegment;
|
|
534
|
+
|
|
535
|
+
// Calculate box position (centered on label coordinates)
|
|
536
|
+
var fBoxX = fLabelX - LABEL_BOX_WIDTH / 2;
|
|
537
|
+
var fBoxY = fLabelY - LABEL_BOX_HEIGHT / 2;
|
|
538
|
+
|
|
539
|
+
// Create a group to contain both rectangle and text
|
|
540
|
+
oRm.openStart("g");
|
|
541
|
+
oRm.class("sapSuiteUiCommonsNetworkLineTextBox");
|
|
542
|
+
oRm.attr("id", sId + "-textbox-middle");
|
|
543
|
+
oRm.attr("data-line-id", this._getLineId());
|
|
544
|
+
oRm.openEnd();
|
|
545
|
+
|
|
546
|
+
// Add title for tooltip on the label box group
|
|
547
|
+
oRm.openStart("title");
|
|
548
|
+
oRm.openEnd();
|
|
549
|
+
oRm.text(sFullText);
|
|
550
|
+
oRm.close("title");
|
|
551
|
+
|
|
552
|
+
// Render the rectangle background
|
|
553
|
+
oRm.openStart("rect");
|
|
554
|
+
oRm.class("sapSuiteUiCommonsNetworkLineTextBoxBackground " + sStatusClass);
|
|
555
|
+
//this.applyStyles(oRm, this.getStyleObject(sStyle)); //ToDo: to be applied if Box should also adhere to status styles
|
|
556
|
+
oRm.attr("x", fBoxX);
|
|
557
|
+
oRm.attr("y", fBoxY);
|
|
558
|
+
oRm.attr("width", LABEL_BOX_WIDTH);
|
|
559
|
+
oRm.attr("height", LABEL_BOX_HEIGHT);
|
|
560
|
+
oRm.attr("rx", 8); // Rounded corners
|
|
561
|
+
oRm.attr("ry", 8);
|
|
562
|
+
oRm.openEnd();
|
|
563
|
+
oRm.close("rect");
|
|
564
|
+
|
|
565
|
+
// Render the text inside the rectangle
|
|
566
|
+
oRm.openStart("text");
|
|
567
|
+
oRm.class("sapSuiteUiCommonsNetworkLineText");
|
|
568
|
+
oRm.attr("x", fLabelX);
|
|
569
|
+
oRm.attr("y", fLabelY); // Center vertically in the box
|
|
570
|
+
oRm.openEnd();
|
|
571
|
+
oRm.text(this.getLabelName());
|
|
572
|
+
oRm.close("text");
|
|
573
|
+
|
|
574
|
+
// Close the group
|
|
575
|
+
oRm.close("g");
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
|
|
339
579
|
Line.prototype._renderFocusWrapper = function () {
|
|
340
580
|
var fnAppendFocusLine = function (iShift) {
|
|
341
581
|
var oPath = this._createElement("path", {
|
|
@@ -343,7 +583,19 @@ sap.ui.define([
|
|
|
343
583
|
"class": "sapSuiteUiCommonsNetworkLineFocus"
|
|
344
584
|
});
|
|
345
585
|
|
|
346
|
-
|
|
586
|
+
// Find the first text box group to insert before it
|
|
587
|
+
var oContainer = this.getDomRef();
|
|
588
|
+
var oFirstTextBox = oContainer ? oContainer.querySelector(".sapSuiteUiCommonsNetworkLineTextBox") : null;
|
|
589
|
+
|
|
590
|
+
if (oFirstTextBox) {
|
|
591
|
+
// Insert the focus line before the first text box group
|
|
592
|
+
oContainer.insertBefore(oPath, oFirstTextBox);
|
|
593
|
+
} else {
|
|
594
|
+
// Fallback: append to end if no text boxes found
|
|
595
|
+
if (oContainer) {
|
|
596
|
+
oContainer.appendChild(oPath);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
347
599
|
}.bind(this);
|
|
348
600
|
|
|
349
601
|
if (!this._bFocusRendered) {
|
|
@@ -430,6 +682,33 @@ sap.ui.define([
|
|
|
430
682
|
|
|
431
683
|
sPosition = sPosition || this.getArrowPosition();
|
|
432
684
|
|
|
685
|
+
// Handle multiple directed arrows positioning
|
|
686
|
+
if (sPosition === "multipleSource" || sPosition === "multipleTarget") {
|
|
687
|
+
// Check if line is long enough for the requested arrow position
|
|
688
|
+
var fTotalLineLength = this._calculateTotalLineLength();
|
|
689
|
+
var bCanFitTwoArrows = this._canLineFitTwoArrows(fTotalLineLength);
|
|
690
|
+
|
|
691
|
+
// If line is too short and we're trying to render source arrow, skip it
|
|
692
|
+
if (!bCanFitTwoArrows && sPosition === "multipleSource") {
|
|
693
|
+
// Return default fallback to avoid errors
|
|
694
|
+
return {
|
|
695
|
+
center: {x: oCoords[0].getX(), y: oCoords[0].getY()},
|
|
696
|
+
apex: {x: oCoords[1].getX(), y: oCoords[1].getY()}
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
// Find optimal segment by traveling 44px along the line path
|
|
700
|
+
var sDirection = sPosition === "multipleSource" ? "forward" : "backward";
|
|
701
|
+
var fDistance = bCanFitTwoArrows ? MULTIPLE_ARROW_DISTANCE : Math.min(MULTIPLE_ARROW_DISTANCE, fTotalLineLength * 0.3);
|
|
702
|
+
var oSegmentInfo = this._findOptimalArrowSegment(sDirection, fDistance);
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
center: oSegmentInfo.center,
|
|
706
|
+
apex: oSegmentInfo.apex,
|
|
707
|
+
segmentRatio: oSegmentInfo.ratio,
|
|
708
|
+
isMultipleDirected: true
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
433
712
|
if (this.getBends().length === 0) {
|
|
434
713
|
iHolyIndex = 0;
|
|
435
714
|
} else if (sPosition === ArrowPosition.Start) {
|
|
@@ -526,8 +805,14 @@ sap.ui.define([
|
|
|
526
805
|
|
|
527
806
|
var fnCalcArrowPoint = function (oArrowVertex) {
|
|
528
807
|
var fFixedPosition = FIXED_ARROW_POSITION;
|
|
529
|
-
//
|
|
530
|
-
if (sPosition ===
|
|
808
|
+
// Handle multiple directed arrows with precise positioning along segments
|
|
809
|
+
if (sPosition === "multipleSource" || sPosition === "multipleTarget") {
|
|
810
|
+
// Use the exact position calculated to be precisely 44px away
|
|
811
|
+
oArrowCenter = oFragVector.exactPosition || {
|
|
812
|
+
x: oFragVector.center.x + (oFragVector.apex.x - oFragVector.center.x) * (oFragVector.segmentRatio || 0.5),
|
|
813
|
+
y: oFragVector.center.y + (oFragVector.apex.y - oFragVector.center.y) * (oFragVector.segmentRatio || 0.5)
|
|
814
|
+
};
|
|
815
|
+
} else if (sPosition === ArrowPosition.Middle) { // First calculate where the center of the arrow is
|
|
531
816
|
oArrowCenter = {
|
|
532
817
|
x: (oFragVector.apex.x - oFragVector.center.x) * RELATIVE_ARROW_POSITION + oFragVector.center.x,
|
|
533
818
|
y: (oFragVector.apex.y - oFragVector.center.y) * RELATIVE_ARROW_POSITION + oFragVector.center.y
|
|
@@ -581,19 +866,47 @@ sap.ui.define([
|
|
|
581
866
|
/**
|
|
582
867
|
* @private
|
|
583
868
|
*/
|
|
584
|
-
Line.prototype._getAccessibilityLabel = function () {
|
|
585
|
-
|
|
586
|
-
|
|
869
|
+
Line.prototype._getAccessibilityLabel = function (oGraph) {
|
|
870
|
+
const aSentenceParts = [];
|
|
871
|
+
const sConnectionType = this.getConnectionType();
|
|
872
|
+
const oMapping = oGraph.getConnectionTypeMapping();
|
|
873
|
+
// In RTL mode, left and right sides of nodes are semantically swapped
|
|
874
|
+
const bRTL = this.getParent()._bIsRtl;
|
|
875
|
+
const sResolvedType = bRTL ? (Utils.mRTLSwap[sConnectionType] || sConnectionType) : sConnectionType;
|
|
876
|
+
const sConnectionTypeText = oMapping[sResolvedType];
|
|
877
|
+
const sFromNodeTitle = this.getFromNode().getTitle();
|
|
878
|
+
const sFromNodeText = sFromNodeTitle ? sFromNodeTitle : this.getFromNode().getAltText();
|
|
879
|
+
const sToNodeTitle = this.getToNode().getTitle();
|
|
880
|
+
const sToNodeText = sToNodeTitle ? sToNodeTitle : this.getToNode().getAltText();
|
|
881
|
+
let sConnectionText = oResourceBundle.getText('NETWORK_GRAPH_LINE_ACCESSIBILITY_CONNECTION_FROM_TO', [sFromNodeText, sToNodeText]);
|
|
882
|
+
if (sConnectionTypeText) {
|
|
883
|
+
sConnectionText = `${sConnectionTypeText} ${sConnectionText.charAt(0).toLowerCase()}${sConnectionText.slice(1)}`;
|
|
884
|
+
}
|
|
885
|
+
aSentenceParts.push(sConnectionText);
|
|
587
886
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
887
|
+
const sStatusText = this._getStatusText(oGraph._oStatuses);
|
|
888
|
+
|
|
889
|
+
if (sStatusText) {
|
|
890
|
+
aSentenceParts.push(
|
|
891
|
+
`${oResourceBundle.getText('NETWORK_GRAPH_LINE_ACCESSIBILITY_STATUS')} ${sStatusText}`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (this.getSelected()) {
|
|
896
|
+
aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_SELECTED_NODE'));
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_ACCESSIBILITY_TOGGLE_STATE'));
|
|
900
|
+
|
|
901
|
+
const iIncomingConnectors = this.getFromNode().getChildLines().length;
|
|
902
|
+
if (iIncomingConnectors > 1) {
|
|
903
|
+
aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_LINE_NAVIGATION_ARROW_KEYS', [sFromNodeText]));
|
|
595
904
|
}
|
|
596
|
-
|
|
905
|
+
|
|
906
|
+
aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_LINE_NAVIGATION_TAB_TO_TARGET', [sToNodeText]));
|
|
907
|
+
aSentenceParts.push(oResourceBundle.getText('NETWORK_GRAPH_LINE_NAVIGATION_SHIFT_TAB_TO_SOURCE', [sFromNodeText]));
|
|
908
|
+
|
|
909
|
+
return aSentenceParts.join('. ');
|
|
597
910
|
};
|
|
598
911
|
|
|
599
912
|
/* =========================================================== */
|
|
@@ -791,10 +1104,39 @@ sap.ui.define([
|
|
|
791
1104
|
$line.on("mouseout", function (oEvent) {
|
|
792
1105
|
this._mouseOut();
|
|
793
1106
|
}.bind(this));
|
|
1107
|
+
|
|
1108
|
+
// Setup events for text box
|
|
1109
|
+
var oParent = this.getParent();
|
|
1110
|
+
if (oParent) {
|
|
1111
|
+
var sLineId = this._getLineId();
|
|
1112
|
+
var $textBox = oParent.$("line-texts").find('[data-line-id="' + sLineId + '"]');
|
|
1113
|
+
|
|
1114
|
+
$textBox.on("click", function (oEvent) {
|
|
1115
|
+
this._click({
|
|
1116
|
+
ctrlKey: oEvent.ctrlKey,
|
|
1117
|
+
clientX: oEvent.clientX,
|
|
1118
|
+
clientY: oEvent.clientY
|
|
1119
|
+
});
|
|
1120
|
+
}.bind(this));
|
|
1121
|
+
|
|
1122
|
+
$textBox.on("mouseover", function (oEvent) {
|
|
1123
|
+
this._mouseOver();
|
|
1124
|
+
}.bind(this));
|
|
1125
|
+
|
|
1126
|
+
$textBox.on("mouseout", function (oEvent) {
|
|
1127
|
+
this._mouseOut();
|
|
1128
|
+
}.bind(this));
|
|
1129
|
+
}
|
|
794
1130
|
};
|
|
795
1131
|
|
|
796
1132
|
Line.prototype._mouseOut = function () {
|
|
797
1133
|
this.$().removeClass(this.HIGHLIGHT_CLASS);
|
|
1134
|
+
// Remove highlight class from text box using data-line-id attribute
|
|
1135
|
+
var oParent = this.getParent();
|
|
1136
|
+
if (oParent) {
|
|
1137
|
+
var sLineId = this._getLineId();
|
|
1138
|
+
oParent.$("line-texts").find('[data-line-id="' + sLineId + '"]').removeClass(this.HIGHLIGHT_CLASS);
|
|
1139
|
+
}
|
|
798
1140
|
if (!this.getSelected()) {
|
|
799
1141
|
this._setStatusColors("");
|
|
800
1142
|
}
|
|
@@ -806,6 +1148,12 @@ sap.ui.define([
|
|
|
806
1148
|
if (!this.getSelected() && bExecuteDefault) {
|
|
807
1149
|
this._setStatusColors("Hover");
|
|
808
1150
|
this.$().addClass(this.HIGHLIGHT_CLASS);
|
|
1151
|
+
// Add highlight class to text box using data-line-id attribute
|
|
1152
|
+
var oParent = this.getParent();
|
|
1153
|
+
if (oParent) {
|
|
1154
|
+
var sLineId = this._getLineId();
|
|
1155
|
+
oParent.$("line-texts").find('[data-line-id="' + sLineId + '"]').addClass(this.HIGHLIGHT_CLASS);
|
|
1156
|
+
}
|
|
809
1157
|
}
|
|
810
1158
|
};
|
|
811
1159
|
|
|
@@ -907,10 +1255,37 @@ sap.ui.define([
|
|
|
907
1255
|
|
|
908
1256
|
oParent._aShadedNodes = [];
|
|
909
1257
|
|
|
910
|
-
|
|
1258
|
+
var sFromNodeTitle = oFrom.getTitle();
|
|
911
1259
|
var sFromNodeText = sFromNodeTitle ? sFromNodeTitle : oFrom.getAltText();
|
|
912
1260
|
|
|
913
|
-
|
|
1261
|
+
// Add connection type information (always shown in tooltip)
|
|
1262
|
+
var sConnectionTypeText = "";
|
|
1263
|
+
var sTitle = "";
|
|
1264
|
+
|
|
1265
|
+
if (this.getParent()._isNoopLayout() &&
|
|
1266
|
+
this.getArrowPosition() === ArrowPosition.Both) {
|
|
1267
|
+
|
|
1268
|
+
var sConnectionType = this.getConnectionType();
|
|
1269
|
+
var oMapping = this.getParent().getConnectionTypeMapping();
|
|
1270
|
+
|
|
1271
|
+
if (sConnectionType === ConnectionType.LeftToRight) {
|
|
1272
|
+
sConnectionTypeText = oMapping.LeftToRight || ConnectionType.LeftToRight;
|
|
1273
|
+
} else if (sConnectionType === ConnectionType.RightToLeft) {
|
|
1274
|
+
sConnectionTypeText = oMapping.RightToLeft || ConnectionType.RightToLeft;
|
|
1275
|
+
} else if (sConnectionType === ConnectionType.LeftToLeft) {
|
|
1276
|
+
sConnectionTypeText = oMapping.LeftToLeft || ConnectionType.LeftToLeft;
|
|
1277
|
+
} else if (sConnectionType === ConnectionType.RightToRight) {
|
|
1278
|
+
sConnectionTypeText = oMapping.RightToRight || ConnectionType.RightToRight;
|
|
1279
|
+
} else {
|
|
1280
|
+
sConnectionTypeText = oMapping.RightToLeft || ConnectionType.RightToLeft;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
if (sConnectionTypeText) {
|
|
1284
|
+
sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipText\">" + sConnectionTypeText + "</span><br/>";
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipText\">" + sFromNodeText + "</span>";
|
|
914
1289
|
|
|
915
1290
|
if (this._isBothArrow()) {
|
|
916
1291
|
sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipArrow sapSuiteUiCommonsNetworkGraphLineTooltipDualArrow\"></span>"
|
|
@@ -923,7 +1298,7 @@ sap.ui.define([
|
|
|
923
1298
|
sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipArrow\"></span>" + "</br>";
|
|
924
1299
|
}
|
|
925
1300
|
|
|
926
|
-
|
|
1301
|
+
var sToNodeTitle = oTo.getTitle();
|
|
927
1302
|
var sToNodeText = sToNodeTitle ? sToNodeTitle : oTo.getAltText();
|
|
928
1303
|
|
|
929
1304
|
sTitle += "<span class=\"sapSuiteUiCommonsNetworkGraphLineTooltipText\">" + sToNodeText + "</span>";
|
|
@@ -1092,6 +1467,18 @@ sap.ui.define([
|
|
|
1092
1467
|
if (bFocus) {
|
|
1093
1468
|
this._renderFocusWrapper();
|
|
1094
1469
|
}
|
|
1470
|
+
|
|
1471
|
+
// Apply focus class to text box for blue border
|
|
1472
|
+
var oParent = this.getParent();
|
|
1473
|
+
if (oParent) {
|
|
1474
|
+
var sLineId = this._getLineId();
|
|
1475
|
+
var $textBox = oParent.$("line-texts").find('[data-line-id="' + sLineId + '"]');
|
|
1476
|
+
if (bFocus) {
|
|
1477
|
+
$textBox.addClass(this.FOCUS_CLASS);
|
|
1478
|
+
} else {
|
|
1479
|
+
$textBox.removeClass(this.FOCUS_CLASS);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1095
1482
|
};
|
|
1096
1483
|
|
|
1097
1484
|
Line.prototype._isEndPosition = function () {
|
|
@@ -1108,9 +1495,29 @@ sap.ui.define([
|
|
|
1108
1495
|
if (bCollapse) {
|
|
1109
1496
|
this.$().hide();
|
|
1110
1497
|
this._bIsHidden = true;
|
|
1498
|
+
// Also hide the line text label if it exists
|
|
1499
|
+
this._hideShowLineText(true);
|
|
1111
1500
|
} else if (!this.getToNode()._bIsHidden && !this.getFromNode()._bIsHidden) {
|
|
1112
1501
|
this.$().show();
|
|
1113
1502
|
this._bIsHidden = false;
|
|
1503
|
+
// Also show the line text label if it exists
|
|
1504
|
+
this._hideShowLineText(false);
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
/**
|
|
1509
|
+
* Hides or shows the line text label element
|
|
1510
|
+
* @param {boolean} bHide True to hide, false to show
|
|
1511
|
+
* @private
|
|
1512
|
+
*/
|
|
1513
|
+
Line.prototype._hideShowLineText = function (bHide) {
|
|
1514
|
+
// Use the same ID pattern that was set during _renderLineText
|
|
1515
|
+
// The text box ID is: this.getId() + "-textbox-middle"
|
|
1516
|
+
var sTextBoxId = this.getId() + "-textbox-middle";
|
|
1517
|
+
var oTextBox = document.getElementById(sTextBoxId);
|
|
1518
|
+
|
|
1519
|
+
if (oTextBox) {
|
|
1520
|
+
oTextBox.style.display = bHide ? "none" : "";
|
|
1114
1521
|
}
|
|
1115
1522
|
};
|
|
1116
1523
|
|
|
@@ -1220,10 +1627,10 @@ sap.ui.define([
|
|
|
1220
1627
|
var LINE_TITLE_LENGTH = 25;
|
|
1221
1628
|
var sTitle = this.getTitle() ? (this.getTitle() + " ") : "";
|
|
1222
1629
|
|
|
1223
|
-
|
|
1630
|
+
var sFromNodeTitle = this.getFromNode().getTitle();
|
|
1224
1631
|
var sFromNodeText = sFromNodeTitle ? sFromNodeTitle : this.getFromNode().getAltText();
|
|
1225
1632
|
|
|
1226
|
-
|
|
1633
|
+
var sToNodeTitle = this.getToNode().getTitle();
|
|
1227
1634
|
var sToNodeText = sToNodeTitle ? sToNodeTitle : this.getToNode().getAltText();
|
|
1228
1635
|
|
|
1229
1636
|
return sTitle + "(" + Utils.trimText(sFromNodeText, LINE_TITLE_LENGTH) + " -> "
|
|
@@ -1258,5 +1665,390 @@ sap.ui.define([
|
|
|
1258
1665
|
return false;
|
|
1259
1666
|
};
|
|
1260
1667
|
|
|
1668
|
+
/**
|
|
1669
|
+
* Calculates the total length of the line by summing up all the segment distances.
|
|
1670
|
+
* @returns {number} Total line length in pixels
|
|
1671
|
+
* @private
|
|
1672
|
+
*/
|
|
1673
|
+
Line.prototype._calculateTotalLineLength = function () {
|
|
1674
|
+
var oCoords = this.getCoordinates();
|
|
1675
|
+
var fTotalLength = 0;
|
|
1676
|
+
|
|
1677
|
+
if (!oCoords || oCoords.length < 2) {
|
|
1678
|
+
return 0;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// Sum the distance of each line segment
|
|
1682
|
+
for (var i = 0; i < oCoords.length - 1; i++) {
|
|
1683
|
+
var fSegmentLength = Math.sqrt(
|
|
1684
|
+
Math.pow(oCoords[i + 1].getX() - oCoords[i].getX(), 2) +
|
|
1685
|
+
Math.pow(oCoords[i + 1].getY() - oCoords[i].getY(), 2)
|
|
1686
|
+
);
|
|
1687
|
+
fTotalLength += fSegmentLength;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
return fTotalLength;
|
|
1691
|
+
};
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Determines if a line is long enough to accommodate two arrows at 44px from each anchor
|
|
1695
|
+
* @param {number} fLineLength - Total length of the line
|
|
1696
|
+
* @returns {boolean} True if line can fit two arrows with proper spacing
|
|
1697
|
+
* @private
|
|
1698
|
+
*/
|
|
1699
|
+
Line.prototype._canLineFitTwoArrows = function (fLineLength) {
|
|
1700
|
+
// Minimum line length calculation:
|
|
1701
|
+
// - 44px from source anchor
|
|
1702
|
+
// - 44px from target anchor
|
|
1703
|
+
// - 20px minimum spacing between arrows
|
|
1704
|
+
// Total: 108px minimum
|
|
1705
|
+
var fMinLineLength = (MULTIPLE_ARROW_DISTANCE * 2) + 20;
|
|
1706
|
+
|
|
1707
|
+
// Add some buffer for curved/bent lines where the actual path might be longer
|
|
1708
|
+
// but the arrow positions could still overlap
|
|
1709
|
+
var fSafeLineLength = fMinLineLength + 10; // 118px total
|
|
1710
|
+
|
|
1711
|
+
return fLineLength >= fSafeLineLength;
|
|
1712
|
+
};
|
|
1713
|
+
|
|
1714
|
+
Line.prototype._shouldRenderSourceArrow = function () {
|
|
1715
|
+
if (!this._hasMultipleDirectedArrows()) {
|
|
1716
|
+
return false;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
var oSourceNode = this.getFromNode();
|
|
1720
|
+
if (!oSourceNode) {
|
|
1721
|
+
return true; // Fallback: show arrow if source node not found
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
var aOutgoingLines = oSourceNode.getChildLines();
|
|
1725
|
+
|
|
1726
|
+
// If source node has only one outgoing line, show the arrow
|
|
1727
|
+
if (!aOutgoingLines || aOutgoingLines.length <= 1) {
|
|
1728
|
+
return true;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
// Filter valid outgoing lines first
|
|
1732
|
+
var aValidOutgoingLines = aOutgoingLines.filter(function(oLine) {
|
|
1733
|
+
return oLine && oLine.getVisible() && !oLine._isIgnored();
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
if (aValidOutgoingLines.length <= 1) {
|
|
1737
|
+
return true;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// Group outgoing lines by their direction/side from the source node
|
|
1741
|
+
var oLinesByDirection = this._groupLinesByDirection(aValidOutgoingLines, oSourceNode, "outgoing");
|
|
1742
|
+
|
|
1743
|
+
// Get the direction of this line
|
|
1744
|
+
var sThisLineDirection = this._getLineDirection(this, oSourceNode, "outgoing");
|
|
1745
|
+
|
|
1746
|
+
// Get lines going in the same direction as this line
|
|
1747
|
+
var aLinesInSameDirection = oLinesByDirection[sThisLineDirection] || [];
|
|
1748
|
+
|
|
1749
|
+
if (aLinesInSameDirection.length <= 1) {
|
|
1750
|
+
return true;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// Sort lines by their ID to ensure consistent ordering
|
|
1754
|
+
aLinesInSameDirection.sort(function(a, b) {
|
|
1755
|
+
return a._getLineId().localeCompare(b._getLineId());
|
|
1756
|
+
});
|
|
1757
|
+
|
|
1758
|
+
// Show arrow only on the first line in the sorted list for this direction
|
|
1759
|
+
return aLinesInSameDirection[0] === this;
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1762
|
+
/**
|
|
1763
|
+
* Checks if multiple directed arrows feature is enabled at the graph level
|
|
1764
|
+
* @returns {boolean} True if the feature is enabled
|
|
1765
|
+
* @private
|
|
1766
|
+
*/
|
|
1767
|
+
Line.prototype._hasMultipleDirectedArrows = function () {
|
|
1768
|
+
var oParent = this.getParent();
|
|
1769
|
+
return oParent && oParent._enableMultipleDirectedArrows === true;
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* Groups lines by their direction relative to a node
|
|
1774
|
+
* @param {array} aLines - Array of lines to group
|
|
1775
|
+
* @param {object} oNode - The reference node
|
|
1776
|
+
* @param {string} sType - "outgoing" or "incoming"
|
|
1777
|
+
* @returns {object} Object with direction as key and array of lines as value
|
|
1778
|
+
* @private
|
|
1779
|
+
*/
|
|
1780
|
+
Line.prototype._groupLinesByDirection = function (aLines, oNode, sType) {
|
|
1781
|
+
var oGroupedLines = {
|
|
1782
|
+
"right": [],
|
|
1783
|
+
"left": [],
|
|
1784
|
+
"up": [],
|
|
1785
|
+
"down": []
|
|
1786
|
+
};
|
|
1787
|
+
|
|
1788
|
+
aLines.forEach(function(oLine) {
|
|
1789
|
+
var sDirection = this._getLineDirection(oLine, oNode, sType);
|
|
1790
|
+
if (oGroupedLines[sDirection]) {
|
|
1791
|
+
oGroupedLines[sDirection].push(oLine);
|
|
1792
|
+
}
|
|
1793
|
+
}.bind(this));
|
|
1794
|
+
|
|
1795
|
+
return oGroupedLines;
|
|
1796
|
+
};
|
|
1797
|
+
|
|
1798
|
+
/**
|
|
1799
|
+
* Determines the direction of a line relative to a node based on connection anchors
|
|
1800
|
+
* @param {object} oLine - The line object
|
|
1801
|
+
* @param {object} oNode - The reference node
|
|
1802
|
+
* @param {string} sType - "outgoing" or "incoming"
|
|
1803
|
+
* @returns {string} Direction: "right", "left", "up", or "down"
|
|
1804
|
+
* @private
|
|
1805
|
+
*/
|
|
1806
|
+
Line.prototype._getLineDirection = function (oLine, oNode, sType) {
|
|
1807
|
+
// Validate inputs
|
|
1808
|
+
if (!oLine || !oNode) {
|
|
1809
|
+
return "right"; // Default fallback
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
var oOtherNode = sType === "outgoing" ? oLine.getToNode() : oLine.getFromNode();
|
|
1813
|
+
if (!oOtherNode) {
|
|
1814
|
+
return "right"; // Default fallback
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Use the actual line coordinates (source/target anchors) instead of node centers
|
|
1818
|
+
var oSourceCoords = oLine.getSource();
|
|
1819
|
+
var oTargetCoords = oLine.getTarget();
|
|
1820
|
+
var oNodeCenter = oNode.getCenterPosition();
|
|
1821
|
+
|
|
1822
|
+
// Validate coordinates
|
|
1823
|
+
if (!oSourceCoords || !oTargetCoords || !oNodeCenter ||
|
|
1824
|
+
typeof oSourceCoords.getX !== 'function' || typeof oSourceCoords.getY !== 'function' ||
|
|
1825
|
+
typeof oTargetCoords.getX !== 'function' || typeof oTargetCoords.getY !== 'function' ||
|
|
1826
|
+
typeof oNodeCenter.x !== 'number' || typeof oNodeCenter.y !== 'number') {
|
|
1827
|
+
return "right"; // Default fallback
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
// Get the anchor point coordinates
|
|
1831
|
+
var fSourceX = oSourceCoords.getX();
|
|
1832
|
+
var fSourceY = oSourceCoords.getY();
|
|
1833
|
+
var fTargetX = oTargetCoords.getX();
|
|
1834
|
+
var fTargetY = oTargetCoords.getY();
|
|
1835
|
+
|
|
1836
|
+
// Determine which anchor point belongs to our reference node
|
|
1837
|
+
var fAnchorX, fAnchorY;
|
|
1838
|
+
if (sType === "outgoing") {
|
|
1839
|
+
// For outgoing lines, use the source anchor (where line starts from our node)
|
|
1840
|
+
fAnchorX = fSourceX;
|
|
1841
|
+
fAnchorY = fSourceY;
|
|
1842
|
+
} else {
|
|
1843
|
+
// For incoming lines, use the target anchor (where line ends at our node)
|
|
1844
|
+
fAnchorX = fTargetX;
|
|
1845
|
+
fAnchorY = fTargetY;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
// Calculate the direction based on anchor position relative to node center
|
|
1849
|
+
var fDeltaX = fAnchorX - oNodeCenter.x;
|
|
1850
|
+
var fDeltaY = fAnchorY - oNodeCenter.y;
|
|
1851
|
+
|
|
1852
|
+
// Use threshold to handle edge cases
|
|
1853
|
+
var fThreshold = 5.0; // 5px threshold for anchor-based detection
|
|
1854
|
+
var fAbsDeltaX = Math.abs(fDeltaX);
|
|
1855
|
+
var fAbsDeltaY = Math.abs(fDeltaY);
|
|
1856
|
+
|
|
1857
|
+
// If anchor is very close to center, fall back to target direction
|
|
1858
|
+
if (fAbsDeltaX < fThreshold && fAbsDeltaY < fThreshold) {
|
|
1859
|
+
// Use direction to other node as fallback
|
|
1860
|
+
var oOtherCenter = oOtherNode.getCenterPosition();
|
|
1861
|
+
if (oOtherCenter) {
|
|
1862
|
+
fDeltaX = oOtherCenter.x - oNodeCenter.x;
|
|
1863
|
+
fDeltaY = oOtherCenter.y - oNodeCenter.y;
|
|
1864
|
+
fAbsDeltaX = Math.abs(fDeltaX);
|
|
1865
|
+
fAbsDeltaY = Math.abs(fDeltaY);
|
|
1866
|
+
} else {
|
|
1867
|
+
return "right"; // Final fallback
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// Determine direction based on larger delta
|
|
1872
|
+
if (fAbsDeltaX > fAbsDeltaY + fThreshold) {
|
|
1873
|
+
return fDeltaX > 0 ? "right" : "left";
|
|
1874
|
+
} else if (fAbsDeltaY > fAbsDeltaX + fThreshold) {
|
|
1875
|
+
return fDeltaY > 0 ? "down" : "up";
|
|
1876
|
+
} else {
|
|
1877
|
+
// When deltas are nearly equal, prefer horizontal direction for consistency
|
|
1878
|
+
return fDeltaX >= 0 ? "right" : "left";
|
|
1879
|
+
}
|
|
1880
|
+
};
|
|
1881
|
+
|
|
1882
|
+
/**
|
|
1883
|
+
* Finds the optimal segment for placing arrows on bent lines by traveling along the path
|
|
1884
|
+
* @param {string} sDirection - "forward" from source or "backward" from target
|
|
1885
|
+
* @param {number} fDistance - Distance to travel along the line (e.g., 44px)
|
|
1886
|
+
* @returns {object} Segment information with exact position 44px away from anchor
|
|
1887
|
+
* @private
|
|
1888
|
+
*/
|
|
1889
|
+
Line.prototype._findOptimalArrowSegment = function (sDirection, fDistance) {
|
|
1890
|
+
var oCoords = this.getCoordinates(),
|
|
1891
|
+
iLastIndex = oCoords.length - 1,
|
|
1892
|
+
fTotalDistance = 0,
|
|
1893
|
+
fSegmentDistance,
|
|
1894
|
+
iSegmentIndex,
|
|
1895
|
+
fRemainingDistance,
|
|
1896
|
+
i;
|
|
1897
|
+
|
|
1898
|
+
// Threshold to detect if arrow is too close to a vertex/bend (10px)
|
|
1899
|
+
var fVertexThreshold = 10;
|
|
1900
|
+
|
|
1901
|
+
if (sDirection === "forward") {
|
|
1902
|
+
// Travel from source towards target
|
|
1903
|
+
for (i = 0; i < iLastIndex; i++) {
|
|
1904
|
+
fSegmentDistance = Math.sqrt(
|
|
1905
|
+
Math.pow(oCoords[i + 1].getX() - oCoords[i].getX(), 2) +
|
|
1906
|
+
Math.pow(oCoords[i + 1].getY() - oCoords[i].getY(), 2)
|
|
1907
|
+
);
|
|
1908
|
+
|
|
1909
|
+
if (fTotalDistance + fSegmentDistance >= fDistance) {
|
|
1910
|
+
// Found the segment where 44px distance falls
|
|
1911
|
+
fRemainingDistance = fDistance - fTotalDistance;
|
|
1912
|
+
iSegmentIndex = i;
|
|
1913
|
+
break;
|
|
1914
|
+
}
|
|
1915
|
+
fTotalDistance += fSegmentDistance;
|
|
1916
|
+
}
|
|
1917
|
+
} else {
|
|
1918
|
+
// Travel from target towards source
|
|
1919
|
+
for (i = iLastIndex; i > 0; i--) {
|
|
1920
|
+
fSegmentDistance = Math.sqrt(
|
|
1921
|
+
Math.pow(oCoords[i].getX() - oCoords[i - 1].getX(), 2) +
|
|
1922
|
+
Math.pow(oCoords[i].getY() - oCoords[i - 1].getY(), 2)
|
|
1923
|
+
);
|
|
1924
|
+
|
|
1925
|
+
if (fTotalDistance + fSegmentDistance >= fDistance) {
|
|
1926
|
+
// Found the segment where 44px distance falls
|
|
1927
|
+
fRemainingDistance = fDistance - fTotalDistance;
|
|
1928
|
+
iSegmentIndex = i - 1; // Use the segment index format (i to i+1)
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
fTotalDistance += fSegmentDistance;
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// Fallback to first/last segment if distance is too large
|
|
1936
|
+
if (typeof iSegmentIndex === "undefined") {
|
|
1937
|
+
iSegmentIndex = sDirection === "forward" ? 0 : Math.max(0, iLastIndex - 1);
|
|
1938
|
+
fRemainingDistance = sDirection === "forward" ? fDistance : fDistance;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// Calculate exact position along the segment
|
|
1942
|
+
var fSegmentLength = Math.sqrt(
|
|
1943
|
+
Math.pow(oCoords[iSegmentIndex + 1].getX() - oCoords[iSegmentIndex].getX(), 2) +
|
|
1944
|
+
Math.pow(oCoords[iSegmentIndex + 1].getY() - oCoords[iSegmentIndex].getY(), 2)
|
|
1945
|
+
);
|
|
1946
|
+
|
|
1947
|
+
var fRatio = fSegmentLength > 0 ? (fRemainingDistance / fSegmentLength) : 0;
|
|
1948
|
+
if (sDirection === "backward") {
|
|
1949
|
+
fRatio = 1 - fRatio; // Reverse the ratio for backward direction
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
// Check if arrow position is too close to a vertex (bend point)
|
|
1953
|
+
var fDistanceToStart = fRatio * fSegmentLength;
|
|
1954
|
+
var fDistanceToEnd = (1 - fRatio) * fSegmentLength;
|
|
1955
|
+
var fAdjustedSegmentLength;
|
|
1956
|
+
|
|
1957
|
+
// Strategy: When arrow is too close to a bend, prefer moving AWAY from the anchor
|
|
1958
|
+
// to avoid overlap with node/port. This means:
|
|
1959
|
+
// - For "forward" direction (from source): prefer moving to next segment (away from source)
|
|
1960
|
+
// - For "backward" direction (from target): prefer moving to previous segment (away from target)
|
|
1961
|
+
|
|
1962
|
+
if (sDirection === "forward") {
|
|
1963
|
+
// Arrow traveling from source: prefer moving forward (away from source) when near bend
|
|
1964
|
+
if (fDistanceToEnd < fVertexThreshold && iSegmentIndex + 1 < iLastIndex) {
|
|
1965
|
+
// Too close to end of segment - move to next segment (preferred)
|
|
1966
|
+
fAdjustedSegmentLength = Math.sqrt(
|
|
1967
|
+
Math.pow(oCoords[iSegmentIndex + 2].getX() - oCoords[iSegmentIndex + 1].getX(), 2) +
|
|
1968
|
+
Math.pow(oCoords[iSegmentIndex + 2].getY() - oCoords[iSegmentIndex + 1].getY(), 2)
|
|
1969
|
+
);
|
|
1970
|
+
|
|
1971
|
+
if (fAdjustedSegmentLength > fVertexThreshold) {
|
|
1972
|
+
// Move arrow to start of next segment plus threshold
|
|
1973
|
+
iSegmentIndex = iSegmentIndex + 1;
|
|
1974
|
+
fRatio = fVertexThreshold / fAdjustedSegmentLength;
|
|
1975
|
+
fSegmentLength = fAdjustedSegmentLength;
|
|
1976
|
+
} else {
|
|
1977
|
+
// Next segment too short, stay on current segment away from vertex
|
|
1978
|
+
fRatio = Math.max(0, (fSegmentLength - fVertexThreshold) / fSegmentLength);
|
|
1979
|
+
}
|
|
1980
|
+
} else if (fDistanceToStart < fVertexThreshold) {
|
|
1981
|
+
// Too close to start of segment - only move back if critically close to source anchor
|
|
1982
|
+
// This is less preferred as it moves closer to the source node/port
|
|
1983
|
+
if (fDistanceToStart < 5 && iSegmentIndex > 0) {
|
|
1984
|
+
// Only if extremely close (< 5px) and previous segment exists
|
|
1985
|
+
fAdjustedSegmentLength = Math.sqrt(
|
|
1986
|
+
Math.pow(oCoords[iSegmentIndex].getX() - oCoords[iSegmentIndex - 1].getX(), 2) +
|
|
1987
|
+
Math.pow(oCoords[iSegmentIndex].getY() - oCoords[iSegmentIndex - 1].getY(), 2)
|
|
1988
|
+
);
|
|
1989
|
+
|
|
1990
|
+
if (fAdjustedSegmentLength > fVertexThreshold) {
|
|
1991
|
+
iSegmentIndex = iSegmentIndex - 1;
|
|
1992
|
+
fRatio = (fAdjustedSegmentLength - fVertexThreshold) / fAdjustedSegmentLength;
|
|
1993
|
+
fSegmentLength = fAdjustedSegmentLength;
|
|
1994
|
+
}
|
|
1995
|
+
} else {
|
|
1996
|
+
// Stay on current segment, move away from start vertex
|
|
1997
|
+
fRatio = Math.min(1, fVertexThreshold / fSegmentLength);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
} else if (sDirection === "backward") {
|
|
2001
|
+
// Arrow traveling from target: prefer moving backward (away from target) when near bend
|
|
2002
|
+
if (fDistanceToStart < fVertexThreshold && iSegmentIndex > 0) {
|
|
2003
|
+
// Too close to start of segment - move to previous segment (preferred)
|
|
2004
|
+
fAdjustedSegmentLength = Math.sqrt(
|
|
2005
|
+
Math.pow(oCoords[iSegmentIndex].getX() - oCoords[iSegmentIndex - 1].getX(), 2) +
|
|
2006
|
+
Math.pow(oCoords[iSegmentIndex].getY() - oCoords[iSegmentIndex - 1].getY(), 2)
|
|
2007
|
+
);
|
|
2008
|
+
|
|
2009
|
+
if (fAdjustedSegmentLength > fVertexThreshold) {
|
|
2010
|
+
// Move arrow to end of previous segment minus threshold
|
|
2011
|
+
iSegmentIndex = iSegmentIndex - 1;
|
|
2012
|
+
fRatio = (fAdjustedSegmentLength - fVertexThreshold) / fAdjustedSegmentLength;
|
|
2013
|
+
fSegmentLength = fAdjustedSegmentLength;
|
|
2014
|
+
} else {
|
|
2015
|
+
// Previous segment too short, stay on current segment away from vertex
|
|
2016
|
+
fRatio = Math.min(1, fVertexThreshold / fSegmentLength);
|
|
2017
|
+
}
|
|
2018
|
+
} else if (fDistanceToEnd < fVertexThreshold) {
|
|
2019
|
+
// Too close to end of segment - only move forward if critically close to target anchor
|
|
2020
|
+
// This is less preferred as it moves closer to the target node/port
|
|
2021
|
+
if (fDistanceToEnd < 5 && iSegmentIndex + 1 < iLastIndex) {
|
|
2022
|
+
// Only if extremely close (< 5px) and next segment exists
|
|
2023
|
+
fAdjustedSegmentLength = Math.sqrt(
|
|
2024
|
+
Math.pow(oCoords[iSegmentIndex + 2].getX() - oCoords[iSegmentIndex + 1].getX(), 2) +
|
|
2025
|
+
Math.pow(oCoords[iSegmentIndex + 2].getY() - oCoords[iSegmentIndex + 1].getY(), 2)
|
|
2026
|
+
);
|
|
2027
|
+
|
|
2028
|
+
if (fAdjustedSegmentLength > fVertexThreshold) {
|
|
2029
|
+
iSegmentIndex = iSegmentIndex + 1;
|
|
2030
|
+
fRatio = fVertexThreshold / fAdjustedSegmentLength;
|
|
2031
|
+
fSegmentLength = fAdjustedSegmentLength;
|
|
2032
|
+
}
|
|
2033
|
+
} else {
|
|
2034
|
+
// Stay on current segment, move away from end vertex
|
|
2035
|
+
fRatio = Math.max(0, (fSegmentLength - fVertexThreshold) / fSegmentLength);
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
// Calculate the exact coordinates with adjusted position
|
|
2041
|
+
var fExactX = oCoords[iSegmentIndex].getX() + (oCoords[iSegmentIndex + 1].getX() - oCoords[iSegmentIndex].getX()) * fRatio;
|
|
2042
|
+
var fExactY = oCoords[iSegmentIndex].getY() + (oCoords[iSegmentIndex + 1].getY() - oCoords[iSegmentIndex].getY()) * fRatio;
|
|
2043
|
+
|
|
2044
|
+
return {
|
|
2045
|
+
segmentIndex: iSegmentIndex,
|
|
2046
|
+
ratio: fRatio,
|
|
2047
|
+
center: {x: oCoords[iSegmentIndex].getX(), y: oCoords[iSegmentIndex].getY()},
|
|
2048
|
+
apex: {x: oCoords[iSegmentIndex + 1].getX(), y: oCoords[iSegmentIndex + 1].getY()},
|
|
2049
|
+
exactPosition: {x: fExactX, y: fExactY}
|
|
2050
|
+
};
|
|
2051
|
+
};
|
|
2052
|
+
|
|
1261
2053
|
return Line;
|
|
1262
2054
|
});
|