@memberjunction/ng-hierarchy-tree 6.1.4 → 6.2.0-edge.1
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.
|
@@ -389,8 +389,24 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
389
389
|
super();
|
|
390
390
|
this.cdr = cdr;
|
|
391
391
|
}
|
|
392
|
-
|
|
393
|
-
|
|
392
|
+
SvgContainerRef;
|
|
393
|
+
/** @deprecated Use {@link SvgContainerRef}. */
|
|
394
|
+
get svgContainerRef() {
|
|
395
|
+
return this.SvgContainerRef;
|
|
396
|
+
}
|
|
397
|
+
/** @deprecated Use {@link SvgContainerRef}. */
|
|
398
|
+
set svgContainerRef(value) {
|
|
399
|
+
this.SvgContainerRef = value;
|
|
400
|
+
}
|
|
401
|
+
SvgRef;
|
|
402
|
+
/** @deprecated Use {@link SvgRef}. */
|
|
403
|
+
get svgRef() {
|
|
404
|
+
return this.SvgRef;
|
|
405
|
+
}
|
|
406
|
+
/** @deprecated Use {@link SvgRef}. */
|
|
407
|
+
set svgRef(value) {
|
|
408
|
+
this.SvgRef = value;
|
|
409
|
+
}
|
|
394
410
|
/**
|
|
395
411
|
* Declarative configuration defining the target entity and visual layout properties.
|
|
396
412
|
*/
|
|
@@ -501,12 +517,12 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
501
517
|
this.loadData();
|
|
502
518
|
}
|
|
503
519
|
ngAfterViewInit() {
|
|
504
|
-
this.log('[HierarchyTree:ngAfterViewInit] Initializing D3 Zoom & ResizeObserver. svgRef:', !!this.
|
|
520
|
+
this.log('[HierarchyTree:ngAfterViewInit] Initializing D3 Zoom & ResizeObserver. svgRef:', !!this.SvgRef?.nativeElement, 'containerRect:', this.SvgContainerRef?.nativeElement?.getBoundingClientRect());
|
|
505
521
|
this.initD3Zoom();
|
|
506
522
|
this.setupResizeObserver();
|
|
507
523
|
if (this.AllNodes.length > 0) {
|
|
508
524
|
this.log('[HierarchyTree:ngAfterViewInit] AllNodes already loaded (' + this.AllNodes.length + '), triggering renderTree');
|
|
509
|
-
this.
|
|
525
|
+
this.RenderTree();
|
|
510
526
|
}
|
|
511
527
|
}
|
|
512
528
|
ngOnChanges(changes) {
|
|
@@ -527,13 +543,13 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
527
543
|
this.updateActiveNodeSelection(this.ActiveRecordID);
|
|
528
544
|
}
|
|
529
545
|
else if (changes['Data'] && !changes['Data'].isFirstChange()) {
|
|
530
|
-
this.
|
|
546
|
+
this.BuildTreeFromData(this.Data || []);
|
|
531
547
|
}
|
|
532
548
|
else if (changes['FocusRecordID'] && !changes['FocusRecordID'].isFirstChange()) {
|
|
533
|
-
this.
|
|
549
|
+
this.SetFocusRoot(this.FocusRecordID);
|
|
534
550
|
}
|
|
535
551
|
else if (changes['ZoomLevel'] && !changes['ZoomLevel'].isFirstChange() && this.ZoomLevel !== undefined) {
|
|
536
|
-
this.
|
|
552
|
+
this.SetZoomLevel(this.ZoomLevel, true);
|
|
537
553
|
}
|
|
538
554
|
}
|
|
539
555
|
hasDataConfigChanged(newConfig) {
|
|
@@ -566,7 +582,7 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
566
582
|
}
|
|
567
583
|
this.SelectedNode = activeNode;
|
|
568
584
|
this.expandAncestors(activeNode);
|
|
569
|
-
this.
|
|
585
|
+
this.RenderTree();
|
|
570
586
|
}
|
|
571
587
|
}
|
|
572
588
|
ngOnDestroy() {
|
|
@@ -639,7 +655,7 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
639
655
|
});
|
|
640
656
|
if (this.Data) {
|
|
641
657
|
this.log('[HierarchyTree:loadData] Using provided Data prop:', this.Data.length, 'items');
|
|
642
|
-
this.
|
|
658
|
+
this.BuildTreeFromData(this.Data);
|
|
643
659
|
return;
|
|
644
660
|
}
|
|
645
661
|
const rv = RunView.FromMetadataProvider(this.ProviderToUse);
|
|
@@ -665,7 +681,7 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
665
681
|
return;
|
|
666
682
|
}
|
|
667
683
|
const rawItems = result.Results || [];
|
|
668
|
-
this.
|
|
684
|
+
this.BuildTreeFromData(rawItems);
|
|
669
685
|
}
|
|
670
686
|
catch (err) {
|
|
671
687
|
this.ErrorMessage = err instanceof Error ? err.message : 'Error loading hierarchy tree.';
|
|
@@ -675,8 +691,8 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
675
691
|
this.Loading = false;
|
|
676
692
|
this.cdr?.detectChanges();
|
|
677
693
|
setTimeout(() => {
|
|
678
|
-
this.
|
|
679
|
-
this.
|
|
694
|
+
this.RenderTree();
|
|
695
|
+
this.FitToScreen(false);
|
|
680
696
|
}, 80);
|
|
681
697
|
}
|
|
682
698
|
}
|
|
@@ -698,7 +714,7 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
698
714
|
/**
|
|
699
715
|
* Constructs the in-memory tree nodes, computes composite keys, descendant counts, and depths.
|
|
700
716
|
*/
|
|
701
|
-
|
|
717
|
+
BuildTreeFromData(items) {
|
|
702
718
|
this.nodeMap.clear();
|
|
703
719
|
this.AllNodes = [];
|
|
704
720
|
const parentFieldName = this.resolveParentFieldName();
|
|
@@ -804,9 +820,13 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
804
820
|
}
|
|
805
821
|
}
|
|
806
822
|
if (this.FocusRecordID || this.Config?.FocusRecordID) {
|
|
807
|
-
this.
|
|
823
|
+
this.SetFocusRoot(this.FocusRecordID || this.Config?.FocusRecordID);
|
|
808
824
|
}
|
|
809
825
|
}
|
|
826
|
+
/** @deprecated Use {@link BuildTreeFromData}. */
|
|
827
|
+
buildTreeFromData(items) {
|
|
828
|
+
return this.BuildTreeFromData(items);
|
|
829
|
+
}
|
|
810
830
|
resolveParentFieldName() {
|
|
811
831
|
if (this.Config?.ParentField)
|
|
812
832
|
return this.Config.ParentField;
|
|
@@ -853,9 +873,9 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
853
873
|
}
|
|
854
874
|
// --- D3 Layout & Visual Rendering ---
|
|
855
875
|
initD3Zoom() {
|
|
856
|
-
if (!this.
|
|
876
|
+
if (!this.SvgRef?.nativeElement)
|
|
857
877
|
return;
|
|
858
|
-
this.svgSelection = d3.select(this.
|
|
878
|
+
this.svgSelection = d3.select(this.SvgRef.nativeElement);
|
|
859
879
|
this.gSelection = this.svgSelection.select('g.mj-hierarchy-canvas');
|
|
860
880
|
this.zoomBehavior = d3.zoom()
|
|
861
881
|
.scaleExtent([0.1, 4.0])
|
|
@@ -872,29 +892,29 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
872
892
|
this.svgSelection.call(this.zoomBehavior);
|
|
873
893
|
}
|
|
874
894
|
setupResizeObserver() {
|
|
875
|
-
if (!this.
|
|
895
|
+
if (!this.SvgContainerRef?.nativeElement)
|
|
876
896
|
return;
|
|
877
897
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
878
898
|
for (const entry of entries) {
|
|
879
899
|
if (entry.contentRect.width > 50 && entry.contentRect.height > 50) {
|
|
880
900
|
if (this.AllNodes.length > 0) {
|
|
881
|
-
this.
|
|
901
|
+
this.RenderTree();
|
|
882
902
|
}
|
|
883
903
|
}
|
|
884
904
|
}
|
|
885
905
|
});
|
|
886
|
-
this.resizeObserver.observe(this.
|
|
906
|
+
this.resizeObserver.observe(this.SvgContainerRef.nativeElement);
|
|
887
907
|
}
|
|
888
908
|
/**
|
|
889
909
|
* Computes the D3 hierarchy tree layout and updates the SVG rendering.
|
|
890
910
|
*/
|
|
891
|
-
|
|
892
|
-
if (!this.
|
|
893
|
-
if (this.
|
|
911
|
+
RenderTree(preserveTransform = false) {
|
|
912
|
+
if (!this.SvgRef?.nativeElement || !this.gSelection) {
|
|
913
|
+
if (this.SvgRef?.nativeElement) {
|
|
894
914
|
this.initD3Zoom();
|
|
895
915
|
}
|
|
896
916
|
if (!this.gSelection) {
|
|
897
|
-
setTimeout(() => this.
|
|
917
|
+
setTimeout(() => this.RenderTree(preserveTransform), 30);
|
|
898
918
|
return;
|
|
899
919
|
}
|
|
900
920
|
}
|
|
@@ -981,14 +1001,18 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
981
1001
|
this.cdr?.markForCheck();
|
|
982
1002
|
this.cdr?.detectChanges();
|
|
983
1003
|
if (!preserveTransform) {
|
|
984
|
-
this.
|
|
1004
|
+
this.FitToScreen(true);
|
|
985
1005
|
}
|
|
986
1006
|
}
|
|
1007
|
+
/** @deprecated Use {@link RenderTree}. */
|
|
1008
|
+
renderTree(preserveTransform = false) {
|
|
1009
|
+
return this.RenderTree(preserveTransform);
|
|
1010
|
+
}
|
|
987
1011
|
// --- Interactive Actions ---
|
|
988
1012
|
/**
|
|
989
1013
|
* Toggles the expand/collapse state of a node, firing cancelable Before and After events.
|
|
990
1014
|
*/
|
|
991
|
-
|
|
1015
|
+
ToggleNodeExpansion(node, event) {
|
|
992
1016
|
if (event)
|
|
993
1017
|
event.stopPropagation();
|
|
994
1018
|
if (node.IsExpanded) {
|
|
@@ -1009,23 +1033,39 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1009
1033
|
node.Children = [...(node._allChildren || [])];
|
|
1010
1034
|
this.AfterNodeExpand.emit({ Node: node, OriginalEvent: event });
|
|
1011
1035
|
}
|
|
1012
|
-
this.
|
|
1036
|
+
this.RenderTree(true);
|
|
1013
1037
|
}
|
|
1014
|
-
|
|
1038
|
+
/** @deprecated Use {@link ToggleNodeExpansion}. */
|
|
1039
|
+
toggleNodeExpansion(node, event) {
|
|
1040
|
+
return this.ToggleNodeExpansion(node, event);
|
|
1041
|
+
}
|
|
1042
|
+
CloseDetailsDrawer() {
|
|
1015
1043
|
this.ShowDetailsDrawer = false;
|
|
1016
1044
|
this.cdr?.markForCheck();
|
|
1017
1045
|
}
|
|
1018
|
-
|
|
1046
|
+
/** @deprecated Use {@link CloseDetailsDrawer}. */
|
|
1047
|
+
closeDetailsDrawer() {
|
|
1048
|
+
return this.CloseDetailsDrawer();
|
|
1049
|
+
}
|
|
1050
|
+
OpenDetailsDrawer() {
|
|
1019
1051
|
this.ShowDetailsDrawer = true;
|
|
1020
1052
|
this.cdr?.markForCheck();
|
|
1021
1053
|
}
|
|
1022
|
-
|
|
1054
|
+
/** @deprecated Use {@link OpenDetailsDrawer}. */
|
|
1055
|
+
openDetailsDrawer() {
|
|
1056
|
+
return this.OpenDetailsDrawer();
|
|
1057
|
+
}
|
|
1058
|
+
OnCanvasBackgroundClick(event) {
|
|
1023
1059
|
const target = event.target;
|
|
1024
1060
|
if (target?.closest('.mj-details-drawer') || target?.closest('.mj-node-card') || target?.closest('.mj-tool-btn')) {
|
|
1025
1061
|
return;
|
|
1026
1062
|
}
|
|
1027
1063
|
}
|
|
1028
|
-
|
|
1064
|
+
/** @deprecated Use {@link OnCanvasBackgroundClick}. */
|
|
1065
|
+
onCanvasBackgroundClick(event) {
|
|
1066
|
+
return this.OnCanvasBackgroundClick(event);
|
|
1067
|
+
}
|
|
1068
|
+
OnNodeClick(node, event) {
|
|
1029
1069
|
event.stopPropagation();
|
|
1030
1070
|
this.SelectedNode = node;
|
|
1031
1071
|
this.ShowDetailsDrawer = true;
|
|
@@ -1036,30 +1076,42 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1036
1076
|
this.NodeSelect.emit({ Node: node, OriginalEvent: event });
|
|
1037
1077
|
this.cdr?.markForCheck();
|
|
1038
1078
|
}
|
|
1039
|
-
|
|
1079
|
+
/** @deprecated Use {@link OnNodeClick}. */
|
|
1080
|
+
onNodeClick(node, event) {
|
|
1081
|
+
return this.OnNodeClick(node, event);
|
|
1082
|
+
}
|
|
1083
|
+
OnNodeDoubleClick(node, event) {
|
|
1040
1084
|
event.stopPropagation();
|
|
1041
1085
|
this.NodeDoubleClick.emit({ Node: node, OriginalEvent: event });
|
|
1042
|
-
this.
|
|
1086
|
+
this.NavigateToRecord(node, event);
|
|
1043
1087
|
}
|
|
1044
|
-
|
|
1088
|
+
/** @deprecated Use {@link OnNodeDoubleClick}. */
|
|
1089
|
+
onNodeDoubleClick(node, event) {
|
|
1090
|
+
return this.OnNodeDoubleClick(node, event);
|
|
1091
|
+
}
|
|
1092
|
+
OnNodeAction(node, action, event) {
|
|
1045
1093
|
event.stopPropagation();
|
|
1046
1094
|
this.NodeAction.emit({ Node: node, Action: action });
|
|
1047
1095
|
switch (action) {
|
|
1048
1096
|
case 'open':
|
|
1049
|
-
this.
|
|
1097
|
+
this.NavigateToRecord(node, event);
|
|
1050
1098
|
break;
|
|
1051
1099
|
case 'focus':
|
|
1052
|
-
this.
|
|
1100
|
+
this.SetFocusRoot(node.ID);
|
|
1053
1101
|
break;
|
|
1054
1102
|
case 'reset-focus':
|
|
1055
|
-
this.
|
|
1103
|
+
this.ResetFocus();
|
|
1056
1104
|
break;
|
|
1057
1105
|
case 'add-child':
|
|
1058
1106
|
// Handled via custom subscriber or event
|
|
1059
1107
|
break;
|
|
1060
1108
|
}
|
|
1061
1109
|
}
|
|
1062
|
-
|
|
1110
|
+
/** @deprecated Use {@link OnNodeAction}. */
|
|
1111
|
+
onNodeAction(node, action, event) {
|
|
1112
|
+
return this.OnNodeAction(node, action, event);
|
|
1113
|
+
}
|
|
1114
|
+
NavigateToRecord(node, event) {
|
|
1063
1115
|
const entityName = this.entityInfo ? this.entityInfo.Name : this.Config.EntityName;
|
|
1064
1116
|
const navEvent = {
|
|
1065
1117
|
Kind: 'record',
|
|
@@ -1069,7 +1121,11 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1069
1121
|
};
|
|
1070
1122
|
this.Navigate.emit(navEvent);
|
|
1071
1123
|
}
|
|
1072
|
-
|
|
1124
|
+
/** @deprecated Use {@link NavigateToRecord}. */
|
|
1125
|
+
navigateToRecord(node, event) {
|
|
1126
|
+
return this.NavigateToRecord(node, event);
|
|
1127
|
+
}
|
|
1128
|
+
GetRecordAttributeEntries(record) {
|
|
1073
1129
|
const entries = [];
|
|
1074
1130
|
const skipFields = new Set([
|
|
1075
1131
|
'id', 'parentid', 'parentproductcategoryid', 'parentcategoryid',
|
|
@@ -1089,15 +1145,19 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1089
1145
|
}
|
|
1090
1146
|
return entries.slice(0, 8);
|
|
1091
1147
|
}
|
|
1148
|
+
/** @deprecated Use {@link GetRecordAttributeEntries}. */
|
|
1149
|
+
getRecordAttributeEntries(record) {
|
|
1150
|
+
return this.GetRecordAttributeEntries(record);
|
|
1151
|
+
}
|
|
1092
1152
|
// --- Search & Highlight ---
|
|
1093
|
-
|
|
1153
|
+
OnSearchInput() {
|
|
1094
1154
|
const q = this.SearchQuery.trim().toLowerCase();
|
|
1095
1155
|
if (!q) {
|
|
1096
1156
|
for (const n of this.AllNodes) {
|
|
1097
1157
|
n.IsHighlighted = false;
|
|
1098
1158
|
}
|
|
1099
1159
|
this.MatchingNodeCount = 0;
|
|
1100
|
-
this.
|
|
1160
|
+
this.RenderTree(true);
|
|
1101
1161
|
return;
|
|
1102
1162
|
}
|
|
1103
1163
|
let count = 0;
|
|
@@ -1113,23 +1173,31 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1113
1173
|
}
|
|
1114
1174
|
}
|
|
1115
1175
|
this.MatchingNodeCount = count;
|
|
1116
|
-
this.
|
|
1176
|
+
this.RenderTree(true);
|
|
1117
1177
|
if (firstMatch && firstMatch.x != null && firstMatch.y != null) {
|
|
1118
|
-
this.
|
|
1178
|
+
this.CenterOnNode(firstMatch);
|
|
1119
1179
|
}
|
|
1120
1180
|
}
|
|
1121
|
-
|
|
1181
|
+
/** @deprecated Use {@link OnSearchInput}. */
|
|
1182
|
+
onSearchInput() {
|
|
1183
|
+
return this.OnSearchInput();
|
|
1184
|
+
}
|
|
1185
|
+
ClearSearch() {
|
|
1122
1186
|
this.SearchQuery = '';
|
|
1123
|
-
this.
|
|
1187
|
+
this.OnSearchInput();
|
|
1124
1188
|
}
|
|
1125
|
-
|
|
1126
|
-
|
|
1189
|
+
/** @deprecated Use {@link ClearSearch}. */
|
|
1190
|
+
clearSearch() {
|
|
1191
|
+
return this.ClearSearch();
|
|
1192
|
+
}
|
|
1193
|
+
CenterOnNode(node) {
|
|
1194
|
+
if (!this.svgSelection || !this.zoomBehavior || !this.SvgContainerRef?.nativeElement)
|
|
1127
1195
|
return;
|
|
1128
1196
|
if (node.x == null || node.y == null)
|
|
1129
1197
|
return;
|
|
1130
|
-
const rect = this.
|
|
1131
|
-
const width = rect.width > 50 ? rect.width : (this.
|
|
1132
|
-
const height = rect.height > 50 ? rect.height : (this.
|
|
1198
|
+
const rect = this.SvgContainerRef.nativeElement.getBoundingClientRect();
|
|
1199
|
+
const width = rect.width > 50 ? rect.width : (this.SvgContainerRef.nativeElement.clientWidth || 800);
|
|
1200
|
+
const height = rect.height > 50 ? rect.height : (this.SvgContainerRef.nativeElement.clientHeight || 500);
|
|
1133
1201
|
const rightMargin = (this.ShowDetailsDrawer && this.SelectedNode) ? 340 : 0;
|
|
1134
1202
|
const visibleWidth = Math.max(width - rightMargin, 200);
|
|
1135
1203
|
const currentScale = this.currentZoomTransform?.k || this.ZoomLevel || 1;
|
|
@@ -1142,6 +1210,10 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1142
1210
|
const target = d3.zoomIdentity.translate(tx, ty).scale(currentScale);
|
|
1143
1211
|
this.svgSelection.interrupt().transition().duration(400).ease(d3.easeCubicOut).call(this.zoomBehavior.transform, target);
|
|
1144
1212
|
}
|
|
1213
|
+
/** @deprecated Use {@link CenterOnNode}. */
|
|
1214
|
+
centerOnNode(node) {
|
|
1215
|
+
return this.CenterOnNode(node);
|
|
1216
|
+
}
|
|
1145
1217
|
expandAncestors(node) {
|
|
1146
1218
|
let curr = node.ParentID ? this.nodeMap.get(NormalizeUUID(node.ParentID)) : undefined;
|
|
1147
1219
|
while (curr) {
|
|
@@ -1151,9 +1223,9 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1151
1223
|
}
|
|
1152
1224
|
}
|
|
1153
1225
|
// --- Subtree Focus ---
|
|
1154
|
-
|
|
1226
|
+
SetFocusRoot(recordId) {
|
|
1155
1227
|
if (!recordId) {
|
|
1156
|
-
this.
|
|
1228
|
+
this.ResetFocus();
|
|
1157
1229
|
return;
|
|
1158
1230
|
}
|
|
1159
1231
|
const target = this.nodeMap.get(NormalizeUUID(recordId));
|
|
@@ -1163,55 +1235,79 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1163
1235
|
n.IsFocusRoot = false;
|
|
1164
1236
|
target.IsFocusRoot = true;
|
|
1165
1237
|
this.FocusedNode = target;
|
|
1166
|
-
this.
|
|
1167
|
-
this.
|
|
1238
|
+
this.RenderTree();
|
|
1239
|
+
this.OnFitToViewClick();
|
|
1168
1240
|
}
|
|
1169
|
-
|
|
1241
|
+
/** @deprecated Use {@link SetFocusRoot}. */
|
|
1242
|
+
setFocusRoot(recordId) {
|
|
1243
|
+
return this.SetFocusRoot(recordId);
|
|
1244
|
+
}
|
|
1245
|
+
ResetFocus() {
|
|
1170
1246
|
for (const n of this.AllNodes)
|
|
1171
1247
|
n.IsFocusRoot = false;
|
|
1172
1248
|
this.FocusedNode = null;
|
|
1173
|
-
this.
|
|
1174
|
-
this.
|
|
1249
|
+
this.RenderTree();
|
|
1250
|
+
this.OnFitToViewClick();
|
|
1251
|
+
}
|
|
1252
|
+
/** @deprecated Use {@link ResetFocus}. */
|
|
1253
|
+
resetFocus() {
|
|
1254
|
+
return this.ResetFocus();
|
|
1175
1255
|
}
|
|
1176
1256
|
// --- Toolbar Actions / Verbs ---
|
|
1177
|
-
|
|
1257
|
+
ExpandAll() {
|
|
1178
1258
|
for (const n of this.AllNodes) {
|
|
1179
1259
|
n.IsExpanded = true;
|
|
1180
1260
|
n.Children = [...(n._allChildren || [])];
|
|
1181
1261
|
}
|
|
1182
|
-
this.
|
|
1262
|
+
this.RenderTree(true);
|
|
1183
1263
|
}
|
|
1184
|
-
|
|
1264
|
+
/** @deprecated Use {@link ExpandAll}. */
|
|
1265
|
+
expandAll() {
|
|
1266
|
+
return this.ExpandAll();
|
|
1267
|
+
}
|
|
1268
|
+
CollapseAll() {
|
|
1185
1269
|
for (const n of this.AllNodes) {
|
|
1186
1270
|
if (n.Depth > 0) {
|
|
1187
1271
|
n.IsExpanded = false;
|
|
1188
1272
|
n.Children = [];
|
|
1189
1273
|
}
|
|
1190
1274
|
}
|
|
1191
|
-
this.
|
|
1275
|
+
this.RenderTree(true);
|
|
1192
1276
|
}
|
|
1193
|
-
|
|
1277
|
+
/** @deprecated Use {@link CollapseAll}. */
|
|
1278
|
+
collapseAll() {
|
|
1279
|
+
return this.CollapseAll();
|
|
1280
|
+
}
|
|
1281
|
+
ZoomIn() {
|
|
1194
1282
|
if (!this.svgSelection || !this.zoomBehavior)
|
|
1195
1283
|
return;
|
|
1196
1284
|
const currentScale = this.currentZoomTransform?.k || 1;
|
|
1197
1285
|
const targetScale = Math.min(currentScale * 1.25, 4.0);
|
|
1198
|
-
this.
|
|
1286
|
+
this.SetZoomLevel(targetScale, true);
|
|
1199
1287
|
this.ZoomLevel = targetScale;
|
|
1200
1288
|
this.ZoomChange.emit(targetScale);
|
|
1201
1289
|
}
|
|
1202
|
-
|
|
1290
|
+
/** @deprecated Use {@link ZoomIn}. */
|
|
1291
|
+
zoomIn() {
|
|
1292
|
+
return this.ZoomIn();
|
|
1293
|
+
}
|
|
1294
|
+
ZoomOut() {
|
|
1203
1295
|
if (!this.svgSelection || !this.zoomBehavior)
|
|
1204
1296
|
return;
|
|
1205
1297
|
const currentScale = this.currentZoomTransform?.k || 1;
|
|
1206
1298
|
const targetScale = Math.max(currentScale * 0.8, 0.1);
|
|
1207
|
-
this.
|
|
1299
|
+
this.SetZoomLevel(targetScale, true);
|
|
1208
1300
|
this.ZoomLevel = targetScale;
|
|
1209
1301
|
this.ZoomChange.emit(targetScale);
|
|
1210
1302
|
}
|
|
1211
|
-
|
|
1303
|
+
/** @deprecated Use {@link ZoomOut}. */
|
|
1304
|
+
zoomOut() {
|
|
1305
|
+
return this.ZoomOut();
|
|
1306
|
+
}
|
|
1307
|
+
SetZoomLevel(scale, animated = true) {
|
|
1212
1308
|
if (!this.svgSelection || !this.zoomBehavior || !scale)
|
|
1213
1309
|
return;
|
|
1214
|
-
const container = this.
|
|
1310
|
+
const container = this.SvgContainerRef?.nativeElement;
|
|
1215
1311
|
const rect = container?.getBoundingClientRect();
|
|
1216
1312
|
const width = rect?.width && rect.width > 50 ? rect.width : (container?.clientWidth || 800);
|
|
1217
1313
|
const height = rect?.height && rect.height > 50 ? rect.height : (container?.clientHeight || 500);
|
|
@@ -1228,19 +1324,31 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1228
1324
|
this.svgSelection.interrupt().call(this.zoomBehavior.transform, target);
|
|
1229
1325
|
}
|
|
1230
1326
|
}
|
|
1231
|
-
|
|
1327
|
+
/** @deprecated Use {@link SetZoomLevel}. */
|
|
1328
|
+
setZoomLevel(scale, animated = true) {
|
|
1329
|
+
return this.SetZoomLevel(scale, animated);
|
|
1330
|
+
}
|
|
1331
|
+
ResetZoom() {
|
|
1232
1332
|
if (!this.svgSelection || !this.zoomBehavior)
|
|
1233
1333
|
return;
|
|
1234
|
-
this.
|
|
1334
|
+
this.OnFitToViewClick();
|
|
1235
1335
|
}
|
|
1236
|
-
|
|
1237
|
-
|
|
1336
|
+
/** @deprecated Use {@link ResetZoom}. */
|
|
1337
|
+
resetZoom() {
|
|
1338
|
+
return this.ResetZoom();
|
|
1339
|
+
}
|
|
1340
|
+
OnFitToViewClick() {
|
|
1341
|
+
this.FitToScreen(false, true);
|
|
1238
1342
|
setTimeout(() => {
|
|
1239
|
-
this.
|
|
1343
|
+
this.FitToScreen(false, true);
|
|
1240
1344
|
}, 320);
|
|
1241
1345
|
}
|
|
1242
|
-
|
|
1243
|
-
|
|
1346
|
+
/** @deprecated Use {@link OnFitToViewClick}. */
|
|
1347
|
+
onFitToViewClick() {
|
|
1348
|
+
return this.OnFitToViewClick();
|
|
1349
|
+
}
|
|
1350
|
+
FitToScreen(immediate = false, forceAutoScale = false) {
|
|
1351
|
+
if (!this.svgSelection || !this.zoomBehavior || !this.SvgContainerRef?.nativeElement)
|
|
1244
1352
|
return;
|
|
1245
1353
|
const visibleNodes = this.AllNodes.filter((n) => n.x != null && n.y != null);
|
|
1246
1354
|
if (visibleNodes.length === 0)
|
|
@@ -1258,9 +1366,9 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1258
1366
|
const treeHeight = Math.max(maxY - minY, 1);
|
|
1259
1367
|
const treeCenterX = (minX + maxX) / 2;
|
|
1260
1368
|
const treeCenterY = (minY + maxY) / 2;
|
|
1261
|
-
const rect = this.
|
|
1262
|
-
const width = rect.width > 50 ? rect.width : (this.
|
|
1263
|
-
const height = rect.height > 50 ? rect.height : (this.
|
|
1369
|
+
const rect = this.SvgContainerRef.nativeElement.getBoundingClientRect();
|
|
1370
|
+
const width = rect.width > 50 ? rect.width : (this.SvgContainerRef.nativeElement.clientWidth || 800);
|
|
1371
|
+
const height = rect.height > 50 ? rect.height : (this.SvgContainerRef.nativeElement.clientHeight || 500);
|
|
1264
1372
|
const rightMargin = (this.ShowDetailsDrawer && this.SelectedNode) ? 340 : 0;
|
|
1265
1373
|
const visibleWidth = Math.max(width - rightMargin, 200);
|
|
1266
1374
|
const padding = 40;
|
|
@@ -1283,19 +1391,31 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1283
1391
|
this.svgSelection.interrupt().transition().duration(300).ease(d3.easeCubicOut).call(this.zoomBehavior.transform, targetTransform);
|
|
1284
1392
|
}
|
|
1285
1393
|
}
|
|
1394
|
+
/** @deprecated Use {@link FitToScreen}. */
|
|
1395
|
+
fitToScreen(immediate = false, forceAutoScale = false) {
|
|
1396
|
+
return this.FitToScreen(immediate, forceAutoScale);
|
|
1397
|
+
}
|
|
1398
|
+
ExportAsSVG() {
|
|
1399
|
+
return this.SvgRef?.nativeElement ? this.SvgRef.nativeElement.outerHTML : '';
|
|
1400
|
+
}
|
|
1401
|
+
/** @deprecated Use {@link ExportAsSVG}. */
|
|
1286
1402
|
exportAsSVG() {
|
|
1287
|
-
return this.
|
|
1403
|
+
return this.ExportAsSVG();
|
|
1288
1404
|
}
|
|
1289
|
-
async
|
|
1405
|
+
async Refresh() {
|
|
1290
1406
|
await this.loadData();
|
|
1291
1407
|
}
|
|
1408
|
+
/** @deprecated Use {@link Refresh}. */
|
|
1409
|
+
async refresh() {
|
|
1410
|
+
return this.Refresh();
|
|
1411
|
+
}
|
|
1292
1412
|
static ɵfac = function HierarchyTreeComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || HierarchyTreeComponent)(i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
|
|
1293
1413
|
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: HierarchyTreeComponent, selectors: [["mj-hierarchy-tree"]], viewQuery: function HierarchyTreeComponent_Query(rf, ctx) { if (rf & 1) {
|
|
1294
1414
|
i0.ɵɵviewQuery(_c0, 5)(_c1, 5);
|
|
1295
1415
|
} if (rf & 2) {
|
|
1296
1416
|
let _t;
|
|
1297
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
1298
|
-
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.
|
|
1417
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.SvgContainerRef = _t.first);
|
|
1418
|
+
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.SvgRef = _t.first);
|
|
1299
1419
|
} }, inputs: { Config: "Config", Data: "Data", ActiveRecordID: "ActiveRecordID", FocusRecordID: "FocusRecordID", AllowNodeActions: "AllowNodeActions", ZoomLevel: "ZoomLevel" }, outputs: { NodeClick: "NodeClick", NodeDoubleClick: "NodeDoubleClick", NodeSelect: "NodeSelect", BeforeNodeExpand: "BeforeNodeExpand", AfterNodeExpand: "AfterNodeExpand", BeforeNodeCollapse: "BeforeNodeCollapse", AfterNodeCollapse: "AfterNodeCollapse", BeforeReparent: "BeforeReparent", AfterReparent: "AfterReparent", NodeAction: "NodeAction", ZoomChange: "ZoomChange", Navigate: "Navigate" }, features: [i0.ɵɵInheritDefinitionFeature, i0.ɵɵNgOnChangesFeature], decls: 20, vars: 9, consts: [["svgContainer", ""], ["svgElement", ""], [1, "mj-hierarchy-wrapper"], [1, "mj-hierarchy-toolbar"], [1, "mj-canvas-container", 3, "click"], [1, "mj-state-overlay"], [1, "mj-state-overlay", "mj-error-state"], [1, "mj-state-overlay", "mj-empty-state"], [1, "mj-hierarchy-svg"], ["id", "mj-tree-grid", "width", "24", "height", "24", "patternUnits", "userSpaceOnUse"], ["cx", "12", "cy", "12", "r", "1", "fill", "var(--mj-border-subtle, #1e293b)", "opacity", "0.4"], ["width", "100%", "height", "100%", "fill", "url(#mj-tree-grid)"], [1, "mj-hierarchy-canvas"], [1, "mj-links-group"], [1, "mj-nodes-group"], ["aria-label", "Node details panel", 1, "mj-details-drawer"], [1, "mj-search-box"], [1, "mj-focus-indicator"], [1, "mj-toolbar-actions"], [1, "fa-solid", "fa-magnifying-glass", "mj-search-icon"], ["type", "text", "placeholder", "Search hierarchy...", 1, "mj-search-input", 3, "ngModelChange", "input", "ngModel"], ["type", "button", "title", "Clear search", 1, "mj-clear-btn"], [1, "mj-match-badge"], ["type", "button", "title", "Clear search", 1, "mj-clear-btn", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "fa-solid", "fa-crosshairs"], ["type", "button", "title", "Show entire hierarchy", 1, "mj-reset-focus-btn", 3, "click"], [1, "fa-solid", "fa-rotate-left"], ["type", "button", "title", "Expand all subtrees", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-folder-open"], ["type", "button", "title", "Collapse all subtrees", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-folder"], [1, "mj-tool-divider"], ["type", "button", "title", "Zoom in", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-plus"], ["type", "button", "title", "Zoom out", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-minus"], ["type", "button", "title", "Fit hierarchy to view", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-expand"], ["type", "button", "title", "Reload hierarchy data", 1, "mj-tool-btn", 3, "click"], [1, "fa-solid", "fa-rotate-right"], [1, "mj-spinner"], [1, "fa-solid", "fa-circle-exclamation", "mj-error-icon"], ["type", "button", 1, "mj-retry-btn", 3, "click"], [1, "fa-solid", "fa-sitemap", "mj-empty-icon"], [1, "mj-node-foreign-object"], [1, "mj-node-card", 3, "click", "dblclick"], [1, "mj-card-accent"], [1, "mj-node-header"], [1, "mj-node-icon"], [1, "mj-node-title-group"], [1, "mj-node-name", 3, "title"], [1, "mj-node-subtitle"], [1, "mj-node-footer"], ["type", "button", 1, "mj-expand-badge", 3, "collapsed", "title"], [1, "mj-leaf-indicator"], [1, "mj-node-actions"], ["type", "button", 1, "mj-expand-badge", 3, "click", "title"], [1, "fa-solid"], ["type", "button", "title", "Focus on this subtree", 1, "mj-node-action-btn", 3, "click"], ["type", "button", "title", "Open record in Explorer", 1, "mj-node-action-btn", 3, "click"], [1, "fa-solid", "fa-arrow-up-right-from-square"], [1, "mj-drawer-header"], [1, "mj-drawer-title-group"], [1, "mj-drawer-icon"], [1, "mj-drawer-titles"], [1, "mj-drawer-name", 3, "title"], [1, "mj-drawer-subtitle"], ["type", "button", "title", "Close details", 1, "mj-drawer-close-btn", 3, "click"], [1, "mj-drawer-body"], [1, "mj-drawer-stats"], [1, "mj-stat-card"], [1, "mj-stat-num"], [1, "mj-stat-label"], [1, "mj-drawer-section"], [1, "mj-drawer-actions"], ["type", "button", "title", "Open this record in Explorer", 1, "mj-drawer-btn-primary", 3, "click"], [1, "mj-drawer-btn-row"], ["type", "button", 1, "mj-drawer-btn-secondary"], ["type", "button", "title", "Focus hierarchy on this subtree", 1, "mj-drawer-btn-secondary", 3, "click"], [1, "mj-section-heading"], [1, "mj-attr-list"], [1, "mj-attr-row"], [1, "mj-attr-dt"], [1, "mj-attr-dd", 3, "title"], ["type", "button", 1, "mj-drawer-btn-secondary", 3, "click"]], template: function HierarchyTreeComponent_Template(rf, ctx) { if (rf & 1) {
|
|
1300
1420
|
i0.ɵɵelementStart(0, "div", 2);
|
|
1301
1421
|
i0.ɵɵconditionalCreate(1, HierarchyTreeComponent_Conditional_1_Template, 4, 3, "div", 3);
|
|
@@ -1333,10 +1453,10 @@ export class HierarchyTreeComponent extends BaseAngularComponent {
|
|
|
1333
1453
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(HierarchyTreeComponent, [{
|
|
1334
1454
|
type: Component,
|
|
1335
1455
|
args: [{ selector: 'mj-hierarchy-tree', standalone: true, imports: [CommonModule, FormsModule], template: "<div class=\"mj-hierarchy-wrapper\" [style.height]=\"Config.Height || '100%'\" [style.min-height]=\"Config.MinHeight || '500px'\">\n <!-- Top Controls Bar -->\n @if (Config.ShowToolbar || Config.ShowSearch) {\n <div class=\"mj-hierarchy-toolbar\">\n <!-- Search Box -->\n @if (Config.ShowSearch) {\n <div class=\"mj-search-box\">\n <i class=\"fa-solid fa-magnifying-glass mj-search-icon\"></i>\n <input\n type=\"text\"\n class=\"mj-search-input\"\n placeholder=\"Search hierarchy...\"\n [(ngModel)]=\"SearchQuery\"\n (input)=\"onSearchInput()\"\n />\n @if (SearchQuery) {\n <button type=\"button\" class=\"mj-clear-btn\" (click)=\"clearSearch()\" title=\"Clear search\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n @if (SearchQuery && MatchingNodeCount > 0) {\n <span class=\"mj-match-badge\">{{ MatchingNodeCount }} found</span>\n }\n </div>\n }\n\n <!-- Focus Subtree Indicator -->\n @if (FocusedNode) {\n <div class=\"mj-focus-indicator\">\n <i class=\"fa-solid fa-crosshairs\"></i>\n <span>Focusing subtree: <strong>{{ FocusedNode.Name }}</strong></span>\n <button type=\"button\" class=\"mj-reset-focus-btn\" (click)=\"resetFocus()\" title=\"Show entire hierarchy\">\n <i class=\"fa-solid fa-rotate-left\"></i> View Full Hierarchy\n </button>\n </div>\n }\n\n <!-- Toolbar Action Buttons -->\n @if (Config.ShowToolbar) {\n <div class=\"mj-toolbar-actions\">\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"expandAll()\" title=\"Expand all subtrees\">\n <i class=\"fa-solid fa-folder-open\"></i>\n </button>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"collapseAll()\" title=\"Collapse all subtrees\">\n <i class=\"fa-solid fa-folder\"></i>\n </button>\n <div class=\"mj-tool-divider\"></div>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"zoomIn()\" title=\"Zoom in\">\n <i class=\"fa-solid fa-plus\"></i>\n </button>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"zoomOut()\" title=\"Zoom out\">\n <i class=\"fa-solid fa-minus\"></i>\n </button>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"onFitToViewClick()\" title=\"Fit hierarchy to view\">\n <i class=\"fa-solid fa-expand\"></i>\n </button>\n <div class=\"mj-tool-divider\"></div>\n <button type=\"button\" class=\"mj-tool-btn\" (click)=\"refresh()\" title=\"Reload hierarchy data\">\n <i class=\"fa-solid fa-rotate-right\"></i>\n </button>\n </div>\n }\n </div>\n }\n\n <!-- SVG Canvas Container -->\n <div #svgContainer class=\"mj-canvas-container\" (click)=\"onCanvasBackgroundClick($event)\">\n @if (Loading) {\n <div class=\"mj-state-overlay\">\n <div class=\"mj-spinner\"></div>\n <span>Loading hierarchy...</span>\n </div>\n } @else if (ErrorMessage) {\n <div class=\"mj-state-overlay mj-error-state\">\n <i class=\"fa-solid fa-circle-exclamation mj-error-icon\"></i>\n <span>{{ ErrorMessage }}</span>\n <button type=\"button\" class=\"mj-retry-btn\" (click)=\"loadData()\">Retry</button>\n </div>\n } @else if (AllNodes.length === 0) {\n <div class=\"mj-state-overlay mj-empty-state\">\n <i class=\"fa-solid fa-sitemap mj-empty-icon\"></i>\n <span>No hierarchy records found.</span>\n </div>\n }\n\n <svg #svgElement class=\"mj-hierarchy-svg\">\n <defs>\n <pattern id=\"mj-tree-grid\" width=\"24\" height=\"24\" patternUnits=\"userSpaceOnUse\">\n <circle cx=\"12\" cy=\"12\" r=\"1\" fill=\"var(--mj-border-subtle, #1e293b)\" opacity=\"0.4\" />\n </pattern>\n </defs>\n\n <!-- Background grid -->\n <rect width=\"100%\" height=\"100%\" fill=\"url(#mj-tree-grid)\" />\n\n <!-- Main Canvas Group (Zoomable & Pannable) -->\n <g class=\"mj-hierarchy-canvas\">\n <!-- Connective Link Curves -->\n <g class=\"mj-links-group\"></g>\n\n <!-- Interactive Node Cards via ForeignObject -->\n <g class=\"mj-nodes-group\">\n @for (node of AllNodes; track node.ID) {\n @if (node.x != null && node.y != null) {\n <foreignObject\n [attr.x]=\"node.x\"\n [attr.y]=\"node.y\"\n [attr.width]=\"Config.NodeWidth || 230\"\n [attr.height]=\"Config.NodeHeight || 90\"\n class=\"mj-node-foreign-object\"\n >\n <div\n class=\"mj-node-card\"\n [class.selected]=\"node.IsSelected\"\n [class.highlighted]=\"node.IsHighlighted\"\n [class.focus-root]=\"node.IsFocusRoot\"\n (click)=\"onNodeClick(node, $event)\"\n (dblclick)=\"onNodeDoubleClick(node, $event)\"\n >\n <!-- Accent Top Border -->\n <div class=\"mj-card-accent\" [style.background]=\"node.Color || Config.DefaultColor\"></div>\n\n <!-- Node Card Header -->\n <div class=\"mj-node-header\">\n <div class=\"mj-node-icon\" [style.color]=\"node.Color || Config.DefaultColor\">\n <i [class]=\"node.Icon || Config.DefaultIcon\"></i>\n </div>\n <div class=\"mj-node-title-group\">\n <div class=\"mj-node-name\" [title]=\"node.Name\">{{ node.Name }}</div>\n @if (node.Subtitle) {\n <div class=\"mj-node-subtitle\">{{ node.Subtitle }}</div>\n }\n </div>\n </div>\n\n <!-- Node Card Footer & Actions -->\n <div class=\"mj-node-footer\">\n <!-- Subtree Expansion Pill -->\n @if (node.DirectChildCount > 0) {\n <button\n type=\"button\"\n class=\"mj-expand-badge\"\n [class.collapsed]=\"!node.IsExpanded\"\n (click)=\"toggleNodeExpansion(node, $event)\"\n [title]=\"node.IsExpanded ? 'Collapse sub-branch' : 'Expand ' + node.DirectChildCount + ' children'\"\n >\n <i class=\"fa-solid\" [class.fa-chevron-down]=\"node.IsExpanded\" [class.fa-chevron-right]=\"!node.IsExpanded\"></i>\n <span>{{ node.DirectChildCount }}</span>\n </button>\n } @else {\n <span class=\"mj-leaf-indicator\">Leaf</span>\n }\n\n <!-- Quick Action Buttons -->\n @if (AllowNodeActions) {\n <div class=\"mj-node-actions\">\n <button\n type=\"button\"\n class=\"mj-node-action-btn\"\n (click)=\"onNodeAction(node, 'focus', $event)\"\n title=\"Focus on this subtree\"\n >\n <i class=\"fa-solid fa-crosshairs\"></i>\n </button>\n <button\n type=\"button\"\n class=\"mj-node-action-btn\"\n (click)=\"onNodeAction(node, 'open', $event)\"\n title=\"Open record in Explorer\"\n >\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n </button>\n </div>\n }\n </div>\n </div>\n </foreignObject>\n }\n }\n </g>\n </g>\n </svg>\n\n <!-- Slide-In Node Details Drawer -->\n <aside\n class=\"mj-details-drawer\"\n [class.open]=\"SelectedNode != null && ShowDetailsDrawer\"\n aria-label=\"Node details panel\"\n >\n @if (SelectedNode) {\n <div class=\"mj-drawer-header\">\n <div class=\"mj-drawer-title-group\">\n <div class=\"mj-drawer-icon\" [style.color]=\"SelectedNode.Color || Config.DefaultColor\">\n <i [class]=\"SelectedNode.Icon || Config.DefaultIcon\"></i>\n </div>\n <div class=\"mj-drawer-titles\">\n <h4 class=\"mj-drawer-name\" [title]=\"SelectedNode.Name\">{{ SelectedNode.Name }}</h4>\n @if (SelectedNode.Subtitle) {\n <p class=\"mj-drawer-subtitle\">{{ SelectedNode.Subtitle }}</p>\n }\n </div>\n </div>\n <button\n type=\"button\"\n class=\"mj-drawer-close-btn\"\n (click)=\"closeDetailsDrawer()\"\n title=\"Close details\"\n >\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n\n <div class=\"mj-drawer-body\">\n <div class=\"mj-drawer-stats\">\n <div class=\"mj-stat-card\">\n <span class=\"mj-stat-num\">{{ SelectedNode.Depth }}</span>\n <span class=\"mj-stat-label\">Hierarchy Level</span>\n </div>\n <div class=\"mj-stat-card\">\n <span class=\"mj-stat-num\">{{ SelectedNode.DirectChildCount }}</span>\n <span class=\"mj-stat-label\">Direct Children</span>\n </div>\n <div class=\"mj-stat-card\">\n <span class=\"mj-stat-num\">{{ SelectedNode.TotalDescendantCount }}</span>\n <span class=\"mj-stat-label\">Total Subtree</span>\n </div>\n </div>\n\n @if (SelectedNode.Record && getRecordAttributeEntries(SelectedNode.Record).length > 0) {\n <div class=\"mj-drawer-section\">\n <h5 class=\"mj-section-heading\">Attributes</h5>\n <dl class=\"mj-attr-list\">\n @for (attr of getRecordAttributeEntries(SelectedNode.Record); track attr.key) {\n <div class=\"mj-attr-row\">\n <dt class=\"mj-attr-dt\">{{ attr.label }}</dt>\n <dd class=\"mj-attr-dd\" [title]=\"attr.value\">{{ attr.value }}</dd>\n </div>\n }\n </dl>\n </div>\n }\n\n <div class=\"mj-drawer-actions\">\n <button\n type=\"button\"\n class=\"mj-drawer-btn-primary\"\n (click)=\"navigateToRecord(SelectedNode)\"\n title=\"Open this record in Explorer\"\n >\n <i class=\"fa-solid fa-arrow-up-right-from-square\"></i>\n <span>Open Record</span>\n </button>\n\n <div class=\"mj-drawer-btn-row\">\n @if (SelectedNode.DirectChildCount > 0) {\n <button\n type=\"button\"\n class=\"mj-drawer-btn-secondary\"\n (click)=\"toggleNodeExpansion(SelectedNode)\"\n >\n <i class=\"fa-solid\" [class.fa-folder-open]=\"SelectedNode.IsExpanded\" [class.fa-folder]=\"!SelectedNode.IsExpanded\"></i>\n <span>{{ SelectedNode.IsExpanded ? 'Collapse' : 'Expand' }}</span>\n </button>\n }\n <button\n type=\"button\"\n class=\"mj-drawer-btn-secondary\"\n (click)=\"setFocusRoot(SelectedNode.ID)\"\n title=\"Focus hierarchy on this subtree\"\n >\n <i class=\"fa-solid fa-crosshairs\"></i>\n <span>Focus Tree</span>\n </button>\n </div>\n </div>\n </div>\n }\n </aside>\n </div>\n</div>\n", styles: [":host {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 500px;\n position: relative;\n box-sizing: border-box;\n font-family: inherit;\n flex: 1;\n}\n\n.mj-hierarchy-wrapper {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: inherit;\n flex: 1;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 12px;\n overflow: hidden;\n position: relative;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);\n}\n\n/* Toolbar */\n.mj-hierarchy-toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n background: var(--mj-bg-surface, #0f172a);\n border-bottom: 1px solid var(--mj-border-default, #1e293b);\n gap: 12px;\n z-index: 10;\n flex-wrap: wrap;\n}\n\n/* Search Box */\n.mj-search-box {\n display: flex;\n align-items: center;\n gap: 8px;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 4px 10px;\n min-width: 240px;\n max-width: 340px;\n flex: 1;\n transition: border-color 0.18s ease;\n}\n\n.mj-search-box:focus-within {\n border-color: var(--mj-brand-primary, #38bdf8);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 18%, transparent);\n}\n\n.mj-search-icon {\n font-size: 12px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-search-input {\n background: transparent;\n border: none;\n outline: none;\n color: var(--mj-text-primary, #f8fafc);\n font-size: 12.5px;\n width: 100%;\n}\n\n.mj-search-input::placeholder {\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-clear-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-muted, #64748b);\n cursor: pointer;\n font-size: 11px;\n padding: 2px 4px;\n border-radius: 4px;\n}\n\n.mj-clear-btn:hover {\n color: var(--mj-text-primary, #f8fafc);\n}\n\n.mj-match-badge {\n font-size: 11px;\n font-weight: 700;\n padding: 2px 7px;\n border-radius: 9999px;\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 15%, transparent);\n color: var(--mj-brand-primary, #38bdf8);\n white-space: nowrap;\n}\n\n/* Focus Subtree Indicator */\n.mj-focus-indicator {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--mj-brand-primary, #38bdf8);\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 10%, transparent);\n border: 1px solid color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 30%, transparent);\n padding: 4px 10px;\n border-radius: 6px;\n}\n\n.mj-reset-focus-btn {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n border-radius: 4px;\n padding: 3px 8px;\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n margin-left: 6px;\n transition: opacity 0.15s ease;\n}\n\n.mj-reset-focus-btn:hover {\n opacity: 0.9;\n}\n\n/* Toolbar Buttons */\n.mj-toolbar-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-left: auto;\n}\n\n.mj-tool-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n background: transparent;\n border: 1px solid var(--mj-border-subtle, #1e293b);\n border-radius: 6px;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 12px;\n cursor: pointer;\n transition: all 0.18s ease;\n}\n\n.mj-tool-btn:hover {\n background: var(--mj-bg-surface-elevated, #1e293b);\n color: var(--mj-text-primary, #f8fafc);\n border-color: var(--mj-border-default, #334155);\n}\n\n.mj-tool-divider {\n width: 1px;\n height: 18px;\n background: var(--mj-border-subtle, #1e293b);\n margin: 0 4px;\n}\n\n/* Canvas Area */\n.mj-canvas-container {\n flex: 1;\n position: relative;\n overflow: hidden;\n width: 100%;\n height: 100%;\n min-height: 450px;\n cursor: grab;\n}\n\n.mj-canvas-container:active {\n cursor: grabbing;\n}\n\n.mj-hierarchy-svg {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n display: block;\n}\n\n/* State Overlays */\n.mj-state-overlay {\n position: absolute;\n inset: 0;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n background: var(--mj-bg-overlay);\n backdrop-filter: blur(4px);\n z-index: 20;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 13px;\n}\n\n.mj-spinner {\n width: 32px;\n height: 32px;\n border: 3px solid color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 20%, transparent);\n border-top-color: var(--mj-brand-primary, #38bdf8);\n border-radius: 50%;\n animation: mj-spin 0.8s linear infinite;\n}\n\n@keyframes mj-spin {\n to { transform: rotate(360deg); }\n}\n\n.mj-error-icon, .mj-empty-icon {\n font-size: 28px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-retry-btn {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n padding: 6px 14px;\n font-size: 12px;\n font-weight: 700;\n border-radius: 6px;\n cursor: pointer;\n}\n\n/* Node ForeignObject Container */\n.mj-node-foreign-object {\n overflow: visible;\n}\n\n/* Interactive Node Card */\n.mj-node-card {\n width: 100%;\n height: 100%;\n box-sizing: border-box;\n background: var(--mj-bg-surface-card, #111a2e);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 10px;\n padding: 8px 12px;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);\n position: relative;\n cursor: pointer;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\n user-select: none;\n}\n\n.mj-node-card:hover {\n border-color: var(--mj-brand-primary, #38bdf8);\n transform: translateY(-2px);\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5), 0 0 12px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 20%, transparent);\n}\n\n.mj-node-card.selected {\n border-color: var(--mj-brand-primary, #38bdf8);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 40%, transparent), 0 6px 20px rgba(0, 0, 0, 0.6);\n}\n\n.mj-node-card.highlighted {\n border-color: var(--mj-status-warning);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-status-warning) 40%, transparent), 0 6px 20px rgba(0, 0, 0, 0.6);\n}\n\n.mj-node-card.focus-root {\n border-color: var(--mj-status-success);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n}\n\n/* Accent Top Bar */\n.mj-card-accent {\n position: absolute;\n top: 0;\n left: 8px;\n right: 8px;\n height: 3px;\n border-radius: 3px 3px 0 0;\n opacity: 0.85;\n}\n\n/* Header */\n.mj-node-header {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-top: 4px;\n}\n\n.mj-node-icon {\n width: 28px;\n height: 28px;\n border-radius: 6px;\n background: rgba(255, 255, 255, 0.05);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n flex-shrink: 0;\n}\n\n.mj-node-title-group {\n display: flex;\n flex-direction: column;\n min-width: 0;\n flex: 1;\n}\n\n.mj-node-name {\n font-size: 12.5px;\n font-weight: 700;\n color: var(--mj-text-primary, #f8fafc);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n.mj-node-subtitle {\n font-size: 11px;\n color: var(--mj-text-muted, #94a3b8);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n/* Footer */\n.mj-node-footer {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding-top: 6px;\n border-top: 1px solid rgba(255, 255, 255, 0.06);\n}\n\n/* Expansion Pill Badge */\n.mj-expand-badge {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n background: var(--mj-bg-surface-elevated, #1a2744);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 9999px;\n padding: 2px 8px;\n color: var(--mj-text-secondary, #94a3b8);\n font-size: 11px;\n font-weight: 700;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-expand-badge:hover {\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border-color: var(--mj-brand-primary, #38bdf8);\n}\n\n.mj-expand-badge.collapsed {\n background: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 15%, transparent);\n color: var(--mj-brand-primary, #38bdf8);\n border-color: color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 35%, transparent);\n}\n\n.mj-leaf-indicator {\n font-size: 10px;\n color: var(--mj-text-muted, #64748b);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n padding-left: 2px;\n}\n\n/* Actions */\n.mj-node-actions {\n display: flex;\n align-items: center;\n gap: 4px;\n}\n\n.mj-node-action-btn {\n width: 22px;\n height: 22px;\n border-radius: 4px;\n background: transparent;\n border: 1px solid transparent;\n color: var(--mj-text-muted, #64748b);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 10.5px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-node-action-btn:hover {\n background: var(--mj-bg-surface-elevated, #1a2744);\n border-color: var(--mj-border-default, #223254);\n color: var(--mj-brand-primary, #38bdf8);\n}\n\n/* ============================================================================\n * Slide-In Node Details Drawer\n * ============================================================================ */\n.mj-details-drawer {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n width: 330px;\n max-width: 85%;\n background: var(--mj-bg-surface, #0f172a);\n border-left: 1px solid var(--mj-border-default, #1e293b);\n box-shadow: -6px 0 24px rgba(0, 0, 0, 0.45);\n z-index: 30;\n display: flex;\n flex-direction: column;\n transform: translateX(100%);\n transition: transform 0.26s cubic-bezier(0.16, 1, 0.3, 1);\n pointer-events: none;\n user-select: text;\n}\n\n.mj-details-drawer.open {\n transform: translateX(0);\n pointer-events: auto;\n}\n\n.mj-drawer-header {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n padding: 16px 18px;\n background: var(--mj-bg-surface-elevated, #151f36);\n border-bottom: 1px solid var(--mj-border-default, #1e293b);\n gap: 12px;\n}\n\n.mj-drawer-title-group {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n flex: 1;\n}\n\n.mj-drawer-icon {\n width: 36px;\n height: 36px;\n border-radius: 8px;\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid rgba(255, 255, 255, 0.08);\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 16px;\n flex-shrink: 0;\n}\n\n.mj-drawer-titles {\n min-width: 0;\n flex: 1;\n}\n\n.mj-drawer-name {\n margin: 0;\n font-size: 15px;\n font-weight: 700;\n color: var(--mj-text-primary, #f8fafc);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n.mj-drawer-subtitle {\n margin: 2px 0 0 0;\n font-size: 12px;\n color: var(--mj-text-muted, #94a3b8);\n line-height: 1.3;\n}\n\n.mj-drawer-close-btn {\n background: transparent;\n border: 1px solid transparent;\n border-radius: 6px;\n width: 28px;\n height: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--mj-text-muted, #94a3b8);\n cursor: pointer;\n font-size: 14px;\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n\n.mj-drawer-close-btn:hover {\n background: rgba(255, 255, 255, 0.08);\n color: var(--mj-text-primary, #f8fafc);\n border-color: var(--mj-border-default, #223254);\n}\n\n.mj-drawer-body {\n flex: 1;\n overflow-y: auto;\n padding: 18px;\n display: flex;\n flex-direction: column;\n gap: 18px;\n}\n\n.mj-drawer-stats {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 8px;\n}\n\n.mj-stat-card {\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 8px 6px;\n display: flex;\n flex-direction: column;\n align-items: center;\n text-align: center;\n}\n\n.mj-stat-num {\n font-size: 16px;\n font-weight: 700;\n color: var(--mj-brand-primary, #38bdf8);\n line-height: 1.2;\n}\n\n.mj-stat-label {\n font-size: 10px;\n font-weight: 600;\n color: var(--mj-text-muted, #64748b);\n text-transform: uppercase;\n letter-spacing: 0.4px;\n margin-top: 2px;\n}\n\n.mj-drawer-section {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.mj-section-heading {\n margin: 0;\n font-size: 11px;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.6px;\n color: var(--mj-text-muted, #64748b);\n}\n\n.mj-attr-list {\n margin: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n background: var(--mj-bg-surface-sunken, #080d1a);\n border: 1px solid var(--mj-border-default, #1e293b);\n border-radius: 8px;\n padding: 10px 12px;\n}\n\n.mj-attr-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.04);\n padding-bottom: 4px;\n}\n\n.mj-attr-row:last-child {\n border-bottom: none;\n padding-bottom: 0;\n}\n\n.mj-attr-dt {\n color: var(--mj-text-muted, #94a3b8);\n font-weight: 500;\n}\n\n.mj-attr-dd {\n margin: 0;\n color: var(--mj-text-primary, #f8fafc);\n font-weight: 600;\n text-align: right;\n max-width: 170px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.mj-drawer-actions {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: auto;\n padding-top: 12px;\n}\n\n.mj-drawer-btn-primary {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n background: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-text-inverse);\n border: none;\n border-radius: 8px;\n padding: 9px 16px;\n font-size: 13px;\n font-weight: 700;\n cursor: pointer;\n transition: all 0.18s ease;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 30%, transparent);\n}\n\n.mj-drawer-btn-primary:hover {\n background: var(--mj-brand-primary-hover);\n box-shadow: 0 4px 14px color-mix(in srgb, var(--mj-brand-primary, #38bdf8) 45%, transparent);\n transform: translateY(-1px);\n}\n\n.mj-drawer-btn-row {\n display: flex;\n gap: 8px;\n}\n\n.mj-drawer-btn-secondary {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n background: var(--mj-bg-surface-elevated, #1a2744);\n border: 1px solid var(--mj-border-default, #223254);\n border-radius: 8px;\n padding: 7px 12px;\n color: var(--mj-text-primary, #f8fafc);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-drawer-btn-secondary:hover {\n background: var(--mj-border-default, #223254);\n border-color: var(--mj-brand-primary, #38bdf8);\n color: var(--mj-brand-primary, #38bdf8);\n}\n"] }]
|
|
1336
|
-
}], () => [{ type: i0.ChangeDetectorRef }], {
|
|
1456
|
+
}], () => [{ type: i0.ChangeDetectorRef }], { SvgContainerRef: [{
|
|
1337
1457
|
type: ViewChild,
|
|
1338
1458
|
args: ['svgContainer', { static: false }]
|
|
1339
|
-
}],
|
|
1459
|
+
}], SvgRef: [{
|
|
1340
1460
|
type: ViewChild,
|
|
1341
1461
|
args: ['svgElement', { static: false }]
|
|
1342
1462
|
}], Config: [{
|