@myrmidon/paged-data-browsers 5.1.1 → 5.1.3

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/README.md CHANGED
@@ -136,6 +136,7 @@ When using an `EditablePagedTreeStoreService`, the store also supports editing o
136
136
  - `clearUnsavedChanges()`: Discard all pending changes
137
137
 
138
138
  All editing operations automatically handle:
139
+
139
140
  - Position recalculation (x coordinates)
140
141
  - Parent `hasChildren` status updates
141
142
  - Cache invalidation
@@ -11,7 +11,7 @@ import { MatBadgeModule } from '@angular/material/badge';
11
11
  import * as i5 from '@angular/material/tooltip';
12
12
  import { MatTooltipModule } from '@angular/material/tooltip';
13
13
  import { ColorToContrastPipe, StringToColorPipe } from '@myrmidon/ngx-tools';
14
- import { BehaviorSubject, of, tap, forkJoin } from 'rxjs';
14
+ import { BehaviorSubject, of, tap, forkJoin, lastValueFrom } from 'rxjs';
15
15
  import { map } from 'rxjs/operators';
16
16
 
17
17
  class CompactPagerComponent {
@@ -52,13 +52,13 @@ class CompactPagerComponent {
52
52
  });
53
53
  }
54
54
  }
55
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: CompactPagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
56
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.1", type: CompactPagerComponent, isStandalone: true, selector: "pdb-compact-pager", inputs: { paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pagingChange: "pagingChange" }, ngImport: i0, template: "@if (paging().pageCount) {\n <div class=\"form-row\">\n <span id=\"pages\">{{ paging().pageNumber }}/{{ paging().pageCount }}</span>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onFirst()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>first_page</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onPrevious()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>navigate_before</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onNext()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>navigate_next</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onLast()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>last_page</mat-icon>\n </button>\n <span id=\"total\">{{ paging().total }} </span>\n </div>\n}\n", styles: ["#pages,#total{color:silver}.form-row{display:flex;gap:2px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] }); }
55
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: CompactPagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
56
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: CompactPagerComponent, isStandalone: true, selector: "pdb-compact-pager", inputs: { paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pagingChange: "pagingChange" }, ngImport: i0, template: "@if (paging().pageCount) {\n <div class=\"form-row\">\n <span id=\"pages\">{{ paging().pageNumber }}/{{ paging().pageCount }}</span>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onFirst()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>first_page</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onPrevious()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>navigate_before</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onNext()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>navigate_next</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onLast()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>last_page</mat-icon>\n </button>\n <span id=\"total\">{{ paging().total }} </span>\n </div>\n}\n", styles: ["#pages,#total{color:silver}.form-row{display:flex;gap:2px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] }); }
57
57
  }
58
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: CompactPagerComponent, decorators: [{
58
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: CompactPagerComponent, decorators: [{
59
59
  type: Component,
60
60
  args: [{ selector: 'pdb-compact-pager', imports: [CommonModule, MatButtonModule, MatIconModule], template: "@if (paging().pageCount) {\n <div class=\"form-row\">\n <span id=\"pages\">{{ paging().pageNumber }}/{{ paging().pageCount }}</span>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onFirst()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>first_page</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onPrevious()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>navigate_before</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onNext()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>navigate_next</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onLast()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>last_page</mat-icon>\n </button>\n <span id=\"total\">{{ paging().total }} </span>\n </div>\n}\n", styles: ["#pages,#total{color:silver}.form-row{display:flex;gap:2px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"] }]
61
- }] });
61
+ }], propDecorators: { paging: [{ type: i0.Input, args: [{ isSignal: true, alias: "paging", required: false }] }], pagingChange: [{ type: i0.Output, args: ["pagingChange"] }] } });
62
62
 
63
63
  class RangeViewComponent {
64
64
  constructor() {
@@ -91,13 +91,13 @@ class RangeViewComponent {
91
91
  ];
92
92
  }, ...(ngDevMode ? [{ debugName: "scaledRange" }] : []));
93
93
  }
94
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: RangeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
95
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.1", type: RangeViewComponent, isStandalone: true, selector: "pdb-range-view", inputs: { domain: { classPropertyName: "domain", publicName: "domain", isSignal: true, isRequired: false, transformFunction: null }, range: { classPropertyName: "range", publicName: "range", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <rect id=\"rdomain\" [attr.width]=\"width()\" [attr.height]=\"height()\" />\n <rect\n id=\"rrange\"\n [attr.x]=\"scaledRange()[0]\"\n [attr.y]=\"0\"\n [attr.width]=\"scaledRange()[1] - scaledRange()[0]\"\n [attr.height]=\"height()\"\n />\n</svg>\n", styles: ["#rdomain{fill:#d3d3d3;stroke-width:3;stroke:#c1ba9b}#rrange{fill:#91aad3;stroke-width:3}\n"] }); }
94
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RangeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
95
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.8", type: RangeViewComponent, isStandalone: true, selector: "pdb-range-view", inputs: { domain: { classPropertyName: "domain", publicName: "domain", isSignal: true, isRequired: false, transformFunction: null }, range: { classPropertyName: "range", publicName: "range", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <rect id=\"rdomain\" [attr.width]=\"width()\" [attr.height]=\"height()\" />\n <rect\n id=\"rrange\"\n [attr.x]=\"scaledRange()[0]\"\n [attr.y]=\"0\"\n [attr.width]=\"scaledRange()[1] - scaledRange()[0]\"\n [attr.height]=\"height()\"\n />\n</svg>\n", styles: ["#rdomain{fill:#d3d3d3;stroke-width:3;stroke:#c1ba9b}#rrange{fill:#91aad3;stroke-width:3}\n"] }); }
96
96
  }
97
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: RangeViewComponent, decorators: [{
97
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: RangeViewComponent, decorators: [{
98
98
  type: Component,
99
99
  args: [{ selector: 'pdb-range-view', template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <rect id=\"rdomain\" [attr.width]=\"width()\" [attr.height]=\"height()\" />\n <rect\n id=\"rrange\"\n [attr.x]=\"scaledRange()[0]\"\n [attr.y]=\"0\"\n [attr.width]=\"scaledRange()[1] - scaledRange()[0]\"\n [attr.height]=\"height()\"\n />\n</svg>\n", styles: ["#rdomain{fill:#d3d3d3;stroke-width:3;stroke:#c1ba9b}#rrange{fill:#91aad3;stroke-width:3}\n"] }]
100
- }], ctorParameters: () => [] });
100
+ }], ctorParameters: () => [], propDecorators: { domain: [{ type: i0.Input, args: [{ isSignal: true, alias: "domain", required: false }] }], range: [{ type: i0.Input, args: [{ isSignal: true, alias: "range", required: false }] }], width: [{ type: i0.Input, args: [{ isSignal: true, alias: "width", required: false }] }], height: [{ type: i0.Input, args: [{ isSignal: true, alias: "height", required: false }] }] } });
101
101
 
102
102
  /**
103
103
  * Browser tree node component view. This wraps some HTML content providing
@@ -182,14 +182,14 @@ class BrowserTreeNodeComponent {
182
182
  this.editNodeFilterRequest.emit(this.node());
183
183
  }
184
184
  }
185
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: BrowserTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
186
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.1", type: BrowserTreeNodeComponent, isStandalone: true, selector: "pdb-browser-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, hideLabel: { classPropertyName: "hideLabel", publicName: "hideLabel", isSignal: true, isRequired: false, transformFunction: null }, hideLoc: { classPropertyName: "hideLoc", publicName: "hideLoc", isSignal: true, isRequired: false, transformFunction: null }, hidePaging: { classPropertyName: "hidePaging", publicName: "hidePaging", isSignal: true, isRequired: false, transformFunction: null }, hideFilter: { classPropertyName: "hideFilter", publicName: "hideFilter", isSignal: true, isRequired: false, transformFunction: null }, indentSize: { classPropertyName: "indentSize", publicName: "indentSize", isSignal: true, isRequired: false, transformFunction: null }, rangeWidth: { classPropertyName: "rangeWidth", publicName: "rangeWidth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggleExpandedRequest: "toggleExpandedRequest", changePageRequest: "changePageRequest", editNodeFilterRequest: "editNodeFilterRequest" }, ngImport: i0, template: "@if (node()) {\n<div id=\"node\" [style.margin-left.px]=\"(node()!.y - 1) * indentSize()\">\n <!-- pager -->\n @if (node()!.expanded && paging() && paging()!.pageCount > 1) {\n <div id=\"pager\" [style.display]=\"hidePaging() ? 'inherit' : 'block'\">\n <pdb-compact-pager\n [paging]=\"paging()!\"\n (pagingChange)=\"onPagingChange(node()!, $event)\"\n />\n <pdb-range-view\n [width]=\"rangeWidth()\"\n [domain]=\"[0, paging()!.pageCount]\"\n [range]=\"[paging()!.pageNumber - 1, paging()!.pageNumber]\"\n />\n </div>\n }\n <!-- node -->\n <div class=\"form-row\">\n <!-- expand/collapse button -->\n <button\n type=\"button\"\n mat-icon-button\n [matTooltip]=\"node()?.expanded ? 'Collapse' : 'Expand'\"\n i18n-matTooltip\n [disabled]=\"node()!.hasChildren === false\"\n (click)=\"onToggleExpanded()\"\n >\n <mat-icon class=\"mat-primary\" i18n>\n @if (node()!.hasChildren === true) { @if (node()!.expanded) {\n chevron_left } @else { chevron_right } } @else { stop }\n </mat-icon>\n </button>\n\n <!-- tag -->\n @if (!hideLabel()) {\n <span\n class=\"tag\"\n [ngStyle]=\"{\n 'background-color': (node()!.tag | stringToColor),\n color: node()!.tag | stringToColor | colorToContrast\n }\"\n >{{ node()!.tag }}</span\n >\n\n <!-- loc and label -->\n @if (!hideLoc()) {\n <span class=\"loc\">{{ node()!.y }}.{{ node()!.x }}</span> - }\n {{ node()!.label }}\n }\n\n <!-- PROJECTED NODE -->\n <ng-content></ng-content>\n\n <!-- debug -->\n @if (debug()) {\n <span class=\"debug\"\n >#{{ node()!.id }}\n <span\n >| {{ node()!.paging.pageNumber }}/{{ node()!.paging.pageCount }} ({{\n node()!.paging.total\n }})</span\n ></span\n >\n }\n\n <!-- filter -->\n @if (!hideFilter()){ @if (!node()?.filter && node()?.y) {\n <div class=\"muted\">\n <button\n type=\"button\"\n mat-icon-button\n matTooltip=\"Add filter\"\n i18n-matTooltip\n (click)=\"onEditFilter()\"\n >\n <mat-icon>filter_list</mat-icon>\n </button>\n </div>\n } @if (node()?.filter && node()?.y) {\n <div class=\"muted\">\n <button type=\"button\" mat-icon-button (click)=\"onEditFilter()\">\n <mat-icon [matBadge]=\"node()?.filter ? 'F' : ''\">filter_alt</mat-icon>\n </button>\n </div>\n } }\n </div>\n</div>\n}\n", styles: [":root{--browser-tree-node-margin-bottom: 4px;--browser-tree-node-padding: 4px 6px;--browser-tree-node-border: 1px solid #98a8d4;--browser-tree-node-border-radius: 6px;--browser-tree-node-hover-bg-color: #d6dee9}#node{margin-bottom:var(--browser-tree-node-margin-bottom);padding:var(--browser-tree-node-padding);border:var(--browser-tree-node-border);border-radius:var(--browser-tree-node-border-radius)}#node:hover{background-color:var(--browser-tree-node-hover-bg-color)}.form-row{display:flex;gap:8px;align-items:center}.form-row *{flex:0 0 auto}.form-row span{flex:0 1 auto;white-space:normal}#node #pager{display:none}#node:hover #pager{display:block}span.loc{font-size:.85em;color:#666;vertical-align:middle}span.tag{border:1px solid #aaa;border-radius:4px;padding:0 4px}fieldset{border:1px solid silver;border-radius:6px;padding:4px 6px;margin:4px 0}legend{color:silver}.muted{opacity:.3}.muted:hover{opacity:1}.debug{font-size:.85em;color:#9c3d3e}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: MatBadgeModule }, { kind: "directive", type: i2$1.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type:
185
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BrowserTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
186
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BrowserTreeNodeComponent, isStandalone: true, selector: "pdb-browser-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, hideLabel: { classPropertyName: "hideLabel", publicName: "hideLabel", isSignal: true, isRequired: false, transformFunction: null }, hideLoc: { classPropertyName: "hideLoc", publicName: "hideLoc", isSignal: true, isRequired: false, transformFunction: null }, hidePaging: { classPropertyName: "hidePaging", publicName: "hidePaging", isSignal: true, isRequired: false, transformFunction: null }, hideFilter: { classPropertyName: "hideFilter", publicName: "hideFilter", isSignal: true, isRequired: false, transformFunction: null }, indentSize: { classPropertyName: "indentSize", publicName: "indentSize", isSignal: true, isRequired: false, transformFunction: null }, rangeWidth: { classPropertyName: "rangeWidth", publicName: "rangeWidth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggleExpandedRequest: "toggleExpandedRequest", changePageRequest: "changePageRequest", editNodeFilterRequest: "editNodeFilterRequest" }, ngImport: i0, template: "@if (node()) {\r\n<div id=\"node\" [style.margin-left.px]=\"(node()!.y - 1) * indentSize()\" [class.hilite]=\"node()!.hilite\">\r\n <!-- pager -->\r\n @if (node()!.expanded && paging() && paging()!.pageCount > 1) {\r\n <div id=\"pager\" [style.display]=\"hidePaging() ? 'inherit' : 'block'\">\r\n <pdb-compact-pager\r\n [paging]=\"paging()!\"\r\n (pagingChange)=\"onPagingChange(node()!, $event)\"\r\n />\r\n <pdb-range-view\r\n [width]=\"rangeWidth()\"\r\n [domain]=\"[0, paging()!.pageCount]\"\r\n [range]=\"[paging()!.pageNumber - 1, paging()!.pageNumber]\"\r\n />\r\n </div>\r\n }\r\n <!-- node -->\r\n <div class=\"form-row\">\r\n <!-- expand/collapse button -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n [matTooltip]=\"node()?.expanded ? 'Collapse' : 'Expand'\"\r\n i18n-matTooltip\r\n [disabled]=\"node()!.hasChildren === false\"\r\n (click)=\"onToggleExpanded()\"\r\n >\r\n <mat-icon class=\"mat-primary\" i18n>\r\n @if (node()!.hasChildren === true) { @if (node()!.expanded) {\r\n chevron_left } @else { chevron_right } } @else { stop }\r\n </mat-icon>\r\n </button>\r\n\r\n <!-- tag -->\r\n @if (!hideLabel()) {\r\n <span\r\n class=\"tag\"\r\n [ngStyle]=\"{\r\n 'background-color': (node()!.tag | stringToColor),\r\n color: node()!.tag | stringToColor | colorToContrast\r\n }\"\r\n >{{ node()!.tag }}</span\r\n >\r\n\r\n <!-- loc and label -->\r\n @if (!hideLoc()) {\r\n <span class=\"loc\">{{ node()!.y }}.{{ node()!.x }}</span> - }\r\n {{ node()!.label }}\r\n }\r\n\r\n <!-- PROJECTED NODE -->\r\n <ng-content></ng-content>\r\n\r\n <!-- debug -->\r\n @if (debug()) {\r\n <span class=\"debug\"\r\n >#{{ node()!.id }}\r\n <span\r\n >| {{ node()!.paging.pageNumber }}/{{ node()!.paging.pageCount }} ({{\r\n node()!.paging.total\r\n }})</span\r\n ></span\r\n >\r\n }\r\n\r\n <!-- filter -->\r\n @if (!hideFilter()){ @if (!node()?.filter && node()?.y) {\r\n <div class=\"muted\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Add filter\"\r\n i18n-matTooltip\r\n (click)=\"onEditFilter()\"\r\n >\r\n <mat-icon>filter_list</mat-icon>\r\n </button>\r\n </div>\r\n } @if (node()?.filter && node()?.y) {\r\n <div class=\"muted\">\r\n <button type=\"button\" mat-icon-button (click)=\"onEditFilter()\">\r\n <mat-icon [matBadge]=\"node()?.filter ? 'F' : ''\">filter_alt</mat-icon>\r\n </button>\r\n </div>\r\n } }\r\n </div>\r\n</div>\r\n}\r\n", styles: [":root{--browser-tree-node-margin-bottom: 4px;--browser-tree-node-padding: 4px 6px;--browser-tree-node-border: 1px solid #98a8d4;--browser-tree-node-border-radius: 6px;--browser-tree-node-hover-bg-color: #d6dee9;--browser-tree-node-hilite-bg-color: #fff3cd;--browser-tree-node-hilite-border: 2px solid #ffc107}#node{margin-bottom:var(--browser-tree-node-margin-bottom);padding:var(--browser-tree-node-padding);border:var(--browser-tree-node-border);border-radius:var(--browser-tree-node-border-radius)}#node:hover{background-color:var(--browser-tree-node-hover-bg-color)}#node.hilite{background-color:#fff3cd!important;border:2px solid #ffc107!important}.form-row{display:flex;gap:8px;align-items:center}.form-row *{flex:0 0 auto}.form-row span{flex:0 1 auto;white-space:normal}#node #pager{display:none}#node:hover #pager{display:block}span.loc{font-size:.85em;color:#666;vertical-align:middle}span.tag{border:1px solid #aaa;border-radius:4px;padding:0 4px}fieldset{border:1px solid silver;border-radius:6px;padding:4px 6px;margin:4px 0}legend{color:silver}.muted{opacity:.3}.muted:hover{opacity:1}.debug{font-size:.85em;color:#9c3d3e}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: MatBadgeModule }, { kind: "directive", type: i2$1.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type:
187
187
  // local
188
188
  CompactPagerComponent, selector: "pdb-compact-pager", inputs: ["paging"], outputs: ["pagingChange"] }, { kind: "component", type: RangeViewComponent, selector: "pdb-range-view", inputs: ["domain", "range", "width", "height"] }, { kind: "pipe", type:
189
189
  // ngx-tools
190
190
  ColorToContrastPipe, name: "colorToContrast" }, { kind: "pipe", type: StringToColorPipe, name: "stringToColor" }] }); }
191
191
  }
192
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: BrowserTreeNodeComponent, decorators: [{
192
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BrowserTreeNodeComponent, decorators: [{
193
193
  type: Component,
194
194
  args: [{ selector: 'pdb-browser-tree-node', imports: [
195
195
  CommonModule,
@@ -203,8 +203,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
203
203
  // local
204
204
  CompactPagerComponent,
205
205
  RangeViewComponent,
206
- ], template: "@if (node()) {\n<div id=\"node\" [style.margin-left.px]=\"(node()!.y - 1) * indentSize()\">\n <!-- pager -->\n @if (node()!.expanded && paging() && paging()!.pageCount > 1) {\n <div id=\"pager\" [style.display]=\"hidePaging() ? 'inherit' : 'block'\">\n <pdb-compact-pager\n [paging]=\"paging()!\"\n (pagingChange)=\"onPagingChange(node()!, $event)\"\n />\n <pdb-range-view\n [width]=\"rangeWidth()\"\n [domain]=\"[0, paging()!.pageCount]\"\n [range]=\"[paging()!.pageNumber - 1, paging()!.pageNumber]\"\n />\n </div>\n }\n <!-- node -->\n <div class=\"form-row\">\n <!-- expand/collapse button -->\n <button\n type=\"button\"\n mat-icon-button\n [matTooltip]=\"node()?.expanded ? 'Collapse' : 'Expand'\"\n i18n-matTooltip\n [disabled]=\"node()!.hasChildren === false\"\n (click)=\"onToggleExpanded()\"\n >\n <mat-icon class=\"mat-primary\" i18n>\n @if (node()!.hasChildren === true) { @if (node()!.expanded) {\n chevron_left } @else { chevron_right } } @else { stop }\n </mat-icon>\n </button>\n\n <!-- tag -->\n @if (!hideLabel()) {\n <span\n class=\"tag\"\n [ngStyle]=\"{\n 'background-color': (node()!.tag | stringToColor),\n color: node()!.tag | stringToColor | colorToContrast\n }\"\n >{{ node()!.tag }}</span\n >\n\n <!-- loc and label -->\n @if (!hideLoc()) {\n <span class=\"loc\">{{ node()!.y }}.{{ node()!.x }}</span> - }\n {{ node()!.label }}\n }\n\n <!-- PROJECTED NODE -->\n <ng-content></ng-content>\n\n <!-- debug -->\n @if (debug()) {\n <span class=\"debug\"\n >#{{ node()!.id }}\n <span\n >| {{ node()!.paging.pageNumber }}/{{ node()!.paging.pageCount }} ({{\n node()!.paging.total\n }})</span\n ></span\n >\n }\n\n <!-- filter -->\n @if (!hideFilter()){ @if (!node()?.filter && node()?.y) {\n <div class=\"muted\">\n <button\n type=\"button\"\n mat-icon-button\n matTooltip=\"Add filter\"\n i18n-matTooltip\n (click)=\"onEditFilter()\"\n >\n <mat-icon>filter_list</mat-icon>\n </button>\n </div>\n } @if (node()?.filter && node()?.y) {\n <div class=\"muted\">\n <button type=\"button\" mat-icon-button (click)=\"onEditFilter()\">\n <mat-icon [matBadge]=\"node()?.filter ? 'F' : ''\">filter_alt</mat-icon>\n </button>\n </div>\n } }\n </div>\n</div>\n}\n", styles: [":root{--browser-tree-node-margin-bottom: 4px;--browser-tree-node-padding: 4px 6px;--browser-tree-node-border: 1px solid #98a8d4;--browser-tree-node-border-radius: 6px;--browser-tree-node-hover-bg-color: #d6dee9}#node{margin-bottom:var(--browser-tree-node-margin-bottom);padding:var(--browser-tree-node-padding);border:var(--browser-tree-node-border);border-radius:var(--browser-tree-node-border-radius)}#node:hover{background-color:var(--browser-tree-node-hover-bg-color)}.form-row{display:flex;gap:8px;align-items:center}.form-row *{flex:0 0 auto}.form-row span{flex:0 1 auto;white-space:normal}#node #pager{display:none}#node:hover #pager{display:block}span.loc{font-size:.85em;color:#666;vertical-align:middle}span.tag{border:1px solid #aaa;border-radius:4px;padding:0 4px}fieldset{border:1px solid silver;border-radius:6px;padding:4px 6px;margin:4px 0}legend{color:silver}.muted{opacity:.3}.muted:hover{opacity:1}.debug{font-size:.85em;color:#9c3d3e}\n"] }]
207
- }] });
206
+ ], template: "@if (node()) {\r\n<div id=\"node\" [style.margin-left.px]=\"(node()!.y - 1) * indentSize()\" [class.hilite]=\"node()!.hilite\">\r\n <!-- pager -->\r\n @if (node()!.expanded && paging() && paging()!.pageCount > 1) {\r\n <div id=\"pager\" [style.display]=\"hidePaging() ? 'inherit' : 'block'\">\r\n <pdb-compact-pager\r\n [paging]=\"paging()!\"\r\n (pagingChange)=\"onPagingChange(node()!, $event)\"\r\n />\r\n <pdb-range-view\r\n [width]=\"rangeWidth()\"\r\n [domain]=\"[0, paging()!.pageCount]\"\r\n [range]=\"[paging()!.pageNumber - 1, paging()!.pageNumber]\"\r\n />\r\n </div>\r\n }\r\n <!-- node -->\r\n <div class=\"form-row\">\r\n <!-- expand/collapse button -->\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n [matTooltip]=\"node()?.expanded ? 'Collapse' : 'Expand'\"\r\n i18n-matTooltip\r\n [disabled]=\"node()!.hasChildren === false\"\r\n (click)=\"onToggleExpanded()\"\r\n >\r\n <mat-icon class=\"mat-primary\" i18n>\r\n @if (node()!.hasChildren === true) { @if (node()!.expanded) {\r\n chevron_left } @else { chevron_right } } @else { stop }\r\n </mat-icon>\r\n </button>\r\n\r\n <!-- tag -->\r\n @if (!hideLabel()) {\r\n <span\r\n class=\"tag\"\r\n [ngStyle]=\"{\r\n 'background-color': (node()!.tag | stringToColor),\r\n color: node()!.tag | stringToColor | colorToContrast\r\n }\"\r\n >{{ node()!.tag }}</span\r\n >\r\n\r\n <!-- loc and label -->\r\n @if (!hideLoc()) {\r\n <span class=\"loc\">{{ node()!.y }}.{{ node()!.x }}</span> - }\r\n {{ node()!.label }}\r\n }\r\n\r\n <!-- PROJECTED NODE -->\r\n <ng-content></ng-content>\r\n\r\n <!-- debug -->\r\n @if (debug()) {\r\n <span class=\"debug\"\r\n >#{{ node()!.id }}\r\n <span\r\n >| {{ node()!.paging.pageNumber }}/{{ node()!.paging.pageCount }} ({{\r\n node()!.paging.total\r\n }})</span\r\n ></span\r\n >\r\n }\r\n\r\n <!-- filter -->\r\n @if (!hideFilter()){ @if (!node()?.filter && node()?.y) {\r\n <div class=\"muted\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Add filter\"\r\n i18n-matTooltip\r\n (click)=\"onEditFilter()\"\r\n >\r\n <mat-icon>filter_list</mat-icon>\r\n </button>\r\n </div>\r\n } @if (node()?.filter && node()?.y) {\r\n <div class=\"muted\">\r\n <button type=\"button\" mat-icon-button (click)=\"onEditFilter()\">\r\n <mat-icon [matBadge]=\"node()?.filter ? 'F' : ''\">filter_alt</mat-icon>\r\n </button>\r\n </div>\r\n } }\r\n </div>\r\n</div>\r\n}\r\n", styles: [":root{--browser-tree-node-margin-bottom: 4px;--browser-tree-node-padding: 4px 6px;--browser-tree-node-border: 1px solid #98a8d4;--browser-tree-node-border-radius: 6px;--browser-tree-node-hover-bg-color: #d6dee9;--browser-tree-node-hilite-bg-color: #fff3cd;--browser-tree-node-hilite-border: 2px solid #ffc107}#node{margin-bottom:var(--browser-tree-node-margin-bottom);padding:var(--browser-tree-node-padding);border:var(--browser-tree-node-border);border-radius:var(--browser-tree-node-border-radius)}#node:hover{background-color:var(--browser-tree-node-hover-bg-color)}#node.hilite{background-color:#fff3cd!important;border:2px solid #ffc107!important}.form-row{display:flex;gap:8px;align-items:center}.form-row *{flex:0 0 auto}.form-row span{flex:0 1 auto;white-space:normal}#node #pager{display:none}#node:hover #pager{display:block}span.loc{font-size:.85em;color:#666;vertical-align:middle}span.tag{border:1px solid #aaa;border-radius:4px;padding:0 4px}fieldset{border:1px solid silver;border-radius:6px;padding:4px 6px;margin:4px 0}legend{color:silver}.muted{opacity:.3}.muted:hover{opacity:1}.debug{font-size:.85em;color:#9c3d3e}\n"] }]
207
+ }], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: false }] }], paging: [{ type: i0.Input, args: [{ isSignal: true, alias: "paging", required: false }] }], debug: [{ type: i0.Input, args: [{ isSignal: true, alias: "debug", required: false }] }], hideLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideLabel", required: false }] }], hideLoc: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideLoc", required: false }] }], hidePaging: [{ type: i0.Input, args: [{ isSignal: true, alias: "hidePaging", required: false }] }], hideFilter: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideFilter", required: false }] }], indentSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "indentSize", required: false }] }], rangeWidth: [{ type: i0.Input, args: [{ isSignal: true, alias: "rangeWidth", required: false }] }], toggleExpandedRequest: [{ type: i0.Output, args: ["toggleExpandedRequest"] }], changePageRequest: [{ type: i0.Output, args: ["changePageRequest"] }], editNodeFilterRequest: [{ type: i0.Output, args: ["editNodeFilterRequest"] }] } });
208
208
 
209
209
  /**
210
210
  * A Least Recently Used cache that can be used to store any type of object.
@@ -648,6 +648,7 @@ class PagedTreeStore {
648
648
  if (this._filter$.value === filter) {
649
649
  return Promise.resolve(false);
650
650
  }
651
+ this.removeHilites();
651
652
  this._filter$.next(filter);
652
653
  this._dirty = true;
653
654
  return this.reset();
@@ -696,6 +697,8 @@ class PagedTreeStore {
696
697
  return Promise.resolve(false);
697
698
  }
698
699
  return new Promise((resolve, reject) => {
700
+ // clear any temporary highlights from ensureNodeVisible
701
+ this.removeHilites();
699
702
  const node = this._nodes$.value.find((n) => n.id === id);
700
703
  if (!node) {
701
704
  reject(`Node ID ${id} not found in store`);
@@ -709,10 +712,15 @@ class PagedTreeStore {
709
712
  * Expand the node with the specified ID. If the node is not expandable,
710
713
  * or it is already expanded, this method does nothing.
711
714
  * @param node The ID of the node to expand.
715
+ * @param silent If true, don't clear hilites (used internally).
712
716
  * @returns Promise with true if the node was expanded, false otherwise.
713
717
  */
714
- expand(id) {
718
+ expand(id, silent = false) {
715
719
  return new Promise((resolve, reject) => {
720
+ // clear any temporary highlights from ensureNodeVisible (unless silent)
721
+ if (!silent) {
722
+ this.removeHilites();
723
+ }
716
724
  // get the node to expand
717
725
  const node = this._nodes$.value.find((n) => n.id === id);
718
726
  // if no node, or no children, or already expanded, do nothing
@@ -829,6 +837,8 @@ class PagedTreeStore {
829
837
  */
830
838
  collapse(id) {
831
839
  return new Promise((resolve, reject) => {
840
+ // clear any temporary highlights from ensureNodeVisible
841
+ this.removeHilites();
832
842
  const node = this._nodes$.value.find((n) => n.id === id);
833
843
  if (!node || node.hasChildren === false || !node.expanded) {
834
844
  resolve(false);
@@ -881,10 +891,15 @@ class PagedTreeStore {
881
891
  * @param parentId The ID of the parent node whose children are inside the page
882
892
  * you want to change.
883
893
  * @param pageNumber The new page number.
894
+ * @param silent If true, don't clear hilites (used internally).
884
895
  * @returns Promise with true if the page was changed, false otherwise.
885
896
  */
886
- changePage(parentId, pageNumber) {
897
+ changePage(parentId, pageNumber, silent = false) {
887
898
  return new Promise((resolve, reject) => {
899
+ // clear any temporary highlights from ensureNodeVisible (unless silent)
900
+ if (!silent) {
901
+ this.removeHilites();
902
+ }
888
903
  // get the parent node
889
904
  const parentNode = this._nodes$.value.find((n) => n.id === parentId);
890
905
  // if parent not found, do nothing
@@ -1007,7 +1022,7 @@ class PagedTreeStore {
1007
1022
  // ensure all matching nodes are visible by expanding their ancestor paths
1008
1023
  const expansionPromises = [];
1009
1024
  for (const node of matchingNodes) {
1010
- expansionPromises.push(this.ensureNodeVisible(node.id));
1025
+ expansionPromises.push(this.ensureAncestorsExpandedInternal(node.id));
1011
1026
  }
1012
1027
  await Promise.all(expansionPromises);
1013
1028
  // update the nodes observable
@@ -1039,9 +1054,9 @@ class PagedTreeStore {
1039
1054
  }
1040
1055
  // if node has children, we need to load them to search through them
1041
1056
  if (node.hasChildren) {
1042
- // Expand the node if not already expanded to load its children
1057
+ // Expand the node if not already expanded to load its children (silently)
1043
1058
  if (!node.expanded) {
1044
- await this.expand(node.id);
1059
+ await this.expand(node.id, true);
1045
1060
  }
1046
1061
  // get all direct children and search them recursively
1047
1062
  const children = this.getChildren(node.id);
@@ -1051,10 +1066,10 @@ class PagedTreeStore {
1051
1066
  }
1052
1067
  }
1053
1068
  /**
1054
- * Ensure a node is visible by expanding all its ancestors.
1055
- * @param nodeId The ID of the node to make visible.
1069
+ * Ensure all ancestors of a node are expanded (used internally by findLabels).
1070
+ * @param nodeId The ID of the node whose ancestors should be expanded.
1056
1071
  */
1057
- async ensureNodeVisible(nodeId) {
1072
+ async ensureAncestorsExpandedInternal(nodeId) {
1058
1073
  const node = this._nodes$.value.find((n) => n.id === nodeId);
1059
1074
  if (!node) {
1060
1075
  return;
@@ -1066,11 +1081,11 @@ class PagedTreeStore {
1066
1081
  ancestorPath.unshift(currentNode.parentId);
1067
1082
  currentNode = this._nodes$.value.find((n) => n.id === currentNode.parentId);
1068
1083
  }
1069
- // expand all ancestors in order
1084
+ // expand all ancestors in order (silently to preserve search hilites)
1070
1085
  for (const ancestorId of ancestorPath) {
1071
1086
  const ancestor = this._nodes$.value.find((n) => n.id === ancestorId);
1072
1087
  if (ancestor && ancestor.hasChildren && !ancestor.expanded) {
1073
- await this.expand(ancestorId);
1088
+ await this.expand(ancestorId, true);
1074
1089
  }
1075
1090
  }
1076
1091
  }
@@ -1209,7 +1224,7 @@ class PagedTreeStore {
1209
1224
  * Coordinate adjustments for non-visible nodes are handled by applyLocalChanges.
1210
1225
  */
1211
1226
  removeNode(nodeId) {
1212
- return new Promise((resolve, reject) => {
1227
+ return new Promise((resolve) => {
1213
1228
  const editableService = this._editableService;
1214
1229
  if (!editableService) {
1215
1230
  resolve(false);
@@ -1280,16 +1295,9 @@ class PagedTreeStore {
1280
1295
  this._nodes$.next(nodes);
1281
1296
  this._dirty = true;
1282
1297
  this.invalidateCache(parentId);
1283
- // reload the parent's current page to show the updated state
1284
- if (parentId !== undefined) {
1285
- const parent = this._nodes$.value.find((n) => n.id === parentId);
1286
- if (parent?.expanded && parent?.paging) {
1287
- this.changePage(parentId, parent.paging.pageNumber)
1288
- .then(() => resolve(true))
1289
- .catch(reject);
1290
- return;
1291
- }
1292
- }
1298
+ // The visible list has already been updated by the deletion logic above.
1299
+ // No need to reload the parent's page as it would cause unnecessary
1300
+ // operations and potential errors if nodes are not found.
1293
1301
  resolve(true);
1294
1302
  });
1295
1303
  }
@@ -1320,6 +1328,10 @@ class PagedTreeStore {
1320
1328
  y: oldNode.y,
1321
1329
  x: oldNode.x,
1322
1330
  };
1331
+ // Ensure the old node is in cache before updating, preserving extended properties
1332
+ if (!editableService.getNode(oldNodeId)) {
1333
+ editableService.updateNode(oldNodeId, oldNode);
1334
+ }
1323
1335
  editableService.updateNode(oldNodeId, updatedNode);
1324
1336
  // handle descendants
1325
1337
  if (!keepDescendants) {
@@ -1455,6 +1467,256 @@ class PagedTreeStore {
1455
1467
  this._editableService?.clearChanges();
1456
1468
  this._dirty = true;
1457
1469
  }
1470
+ /**
1471
+ * Ensure that the node with the specified ID is visible in the tree.
1472
+ * This method will:
1473
+ * 1. Optionally refresh data by clearing cache
1474
+ * 2. Find the node and determine which page it's on
1475
+ * 3. Expand all ancestor nodes to make it visible
1476
+ * 4. Navigate to the page containing the node
1477
+ * 5. Optionally set the node's expanded/collapsed state
1478
+ *
1479
+ * @param id The ID of the node to make visible.
1480
+ * @param expanded If true, expand the node; if false, collapse it; if undefined, don't change.
1481
+ * @param refresh If true, clear cache to reload data from service.
1482
+ * @returns Promise that resolves to true if successful, false otherwise.
1483
+ */
1484
+ async ensureNodeVisible(id, expanded, refresh) {
1485
+ console.log(`[ensureNodeVisible] Starting for node ${id}, expanded=${expanded}, refresh=${refresh}`);
1486
+ // clear cache if refresh is requested
1487
+ if (refresh) {
1488
+ this.clearCache();
1489
+ // also need to reset the tree to reload from service
1490
+ await this.reset();
1491
+ }
1492
+ // try to find the node in the current visible list
1493
+ let node = this._nodes$.value.find((n) => n.id === id);
1494
+ console.log(`[ensureNodeVisible] Node ${id} ${node ? 'found' : 'not found'} in visible list`);
1495
+ // if node is not visible, we need to find it by traversing the tree
1496
+ if (!node) {
1497
+ console.log(`[ensureNodeVisible] Searching for node ${id} in tree...`);
1498
+ const foundNode = await this.findAndLoadNode(id);
1499
+ if (!foundNode) {
1500
+ console.warn(`[ensureNodeVisible] Node ${id} not found in tree`);
1501
+ return false;
1502
+ }
1503
+ node = foundNode;
1504
+ console.log(`[ensureNodeVisible] Node ${id} found via tree search`);
1505
+ }
1506
+ // ensure all ancestors are expanded
1507
+ console.log(`[ensureNodeVisible] Ensuring ancestors of node ${id} are expanded`);
1508
+ await this.ensureAncestorsExpanded(id);
1509
+ // refresh the node reference after expansion
1510
+ node = this._nodes$.value.find((n) => n.id === id);
1511
+ if (!node) {
1512
+ console.warn(`[ensureNodeVisible] Node ${id} not found after ancestor expansion`);
1513
+ return false;
1514
+ }
1515
+ // if the node has a parent, ensure we're on the right page
1516
+ if (node.parentId !== undefined) {
1517
+ const parent = this._nodes$.value.find((n) => n.id === node.parentId);
1518
+ if (parent) {
1519
+ console.log(`[ensureNodeVisible] Finding page for node ${id}`);
1520
+ // find which page the node is on
1521
+ const siblings = await this.getAllSiblings(node);
1522
+ const nodeIndex = siblings.findIndex((n) => n.id === id);
1523
+ if (nodeIndex !== -1) {
1524
+ const pageNumber = Math.floor(nodeIndex / this._pageSize) + 1;
1525
+ console.log(`[ensureNodeVisible] Node ${id} is on page ${pageNumber} (current: ${parent.paging.pageNumber})`);
1526
+ if (parent.paging.pageNumber !== pageNumber) {
1527
+ await this.changePage(parent.id, pageNumber, true); // silent to avoid clearing hilites
1528
+ }
1529
+ }
1530
+ }
1531
+ }
1532
+ // set the expanded state if requested
1533
+ if (expanded !== undefined) {
1534
+ node = this._nodes$.value.find((n) => n.id === id);
1535
+ if (node) {
1536
+ if (expanded && !node.expanded && node.hasChildren) {
1537
+ console.log(`[ensureNodeVisible] Expanding node ${id}`);
1538
+ await this.expand(id, true); // silent to avoid clearing hilites
1539
+ }
1540
+ else if (!expanded && node.expanded) {
1541
+ console.log(`[ensureNodeVisible] Collapsing node ${id}`);
1542
+ await this.collapse(id);
1543
+ }
1544
+ }
1545
+ }
1546
+ // highlight the target node to draw user attention
1547
+ node = this._nodes$.value.find((n) => n.id === id);
1548
+ if (node) {
1549
+ // first remove all existing hilites
1550
+ this.removeHilites();
1551
+ // then highlight the target node
1552
+ node.hilite = true;
1553
+ this._nodes$.next(this._nodes$.value);
1554
+ console.log(`[ensureNodeVisible] Node ${id} highlighted successfully`);
1555
+ }
1556
+ else {
1557
+ console.warn(`[ensureNodeVisible] Node ${id} not found for highlighting`);
1558
+ }
1559
+ return node !== undefined;
1560
+ }
1561
+ /**
1562
+ * Find and load a node that is not currently visible by querying the service
1563
+ * to build the path from root to the target node, then expand only the nodes
1564
+ * on that path.
1565
+ * @param id The ID of the node to find.
1566
+ * @returns The node if found, undefined otherwise.
1567
+ */
1568
+ async findAndLoadNode(id) {
1569
+ console.log(`[findAndLoadNode] Searching for node ${id}`);
1570
+ // Query the service to find the target node and build the ancestor path
1571
+ const path = await this.findNodePath(id);
1572
+ if (!path || path.length === 0) {
1573
+ console.warn(`[findAndLoadNode] No path found for node ${id}`);
1574
+ return undefined;
1575
+ }
1576
+ console.log(`[findAndLoadNode] Path to node ${id}:`, path.map(n => n.id));
1577
+ // Expand nodes along the path
1578
+ for (let i = 0; i < path.length - 1; i++) {
1579
+ const node = path[i];
1580
+ const visibleNode = this._nodes$.value.find((n) => n.id === node.id);
1581
+ if (visibleNode && !visibleNode.expanded) {
1582
+ console.log(`[findAndLoadNode] Expanding node ${node.id} on path`);
1583
+ await this.expand(node.id, true); // silent to avoid clearing hilites
1584
+ }
1585
+ }
1586
+ // Return the target node from the visible list
1587
+ return this._nodes$.value.find((n) => n.id === id);
1588
+ }
1589
+ /**
1590
+ * Find the path from root to a target node by querying the service.
1591
+ * @param targetId The ID of the target node.
1592
+ * @returns Array of nodes from root to target, or undefined if not found.
1593
+ */
1594
+ async findNodePath(targetId) {
1595
+ // Use a breadth-first search to find the node by querying all pages
1596
+ const searchQueue = [];
1597
+ const pathMap = new Map(); // node ID -> path from root
1598
+ // Start with root nodes
1599
+ const rootPage = await lastValueFrom(this._service.getNodes({ ...this._filter$.value, parentId: undefined }, 1, 999999, // get all root nodes
1600
+ this._hasMockRoot));
1601
+ for (const rootNode of rootPage.items) {
1602
+ searchQueue.push(rootNode);
1603
+ pathMap.set(rootNode.id, [rootNode]);
1604
+ if (rootNode.id === targetId) {
1605
+ return [rootNode];
1606
+ }
1607
+ }
1608
+ // BFS through the tree
1609
+ while (searchQueue.length > 0) {
1610
+ const current = searchQueue.shift();
1611
+ const currentPath = pathMap.get(current.id);
1612
+ // Get all children of current node
1613
+ const childrenPage = await lastValueFrom(this._service.getNodes({ ...this._filter$.value, parentId: current.id }, 1, 999999, // get all children
1614
+ this._hasMockRoot));
1615
+ for (const child of childrenPage.items) {
1616
+ const childPath = [...currentPath, child];
1617
+ pathMap.set(child.id, childPath);
1618
+ if (child.id === targetId) {
1619
+ console.log(`[findNodePath] Found target ${targetId}`);
1620
+ return childPath;
1621
+ }
1622
+ searchQueue.push(child);
1623
+ }
1624
+ }
1625
+ return undefined;
1626
+ }
1627
+ /**
1628
+ * Ensure all ancestors of a node are expanded.
1629
+ * @param nodeId The ID of the node whose ancestors should be expanded.
1630
+ */
1631
+ async ensureAncestorsExpanded(nodeId) {
1632
+ const node = this._nodes$.value.find((n) => n.id === nodeId);
1633
+ if (!node) {
1634
+ return;
1635
+ }
1636
+ // find the path from root to this node
1637
+ const ancestorPath = [];
1638
+ let currentNode = node;
1639
+ while (currentNode && currentNode.parentId !== undefined) {
1640
+ ancestorPath.unshift(currentNode.parentId);
1641
+ currentNode = this._nodes$.value.find((n) => n.id === currentNode.parentId);
1642
+ }
1643
+ // expand all ancestors in order (silently to avoid clearing hilites)
1644
+ for (const ancestorId of ancestorPath) {
1645
+ const ancestor = this._nodes$.value.find((n) => n.id === ancestorId);
1646
+ if (ancestor && ancestor.hasChildren && !ancestor.expanded) {
1647
+ await this.expand(ancestorId, true);
1648
+ }
1649
+ }
1650
+ }
1651
+ /**
1652
+ * Get all siblings of a node, including those not currently visible.
1653
+ * @param node The node whose siblings to retrieve.
1654
+ * @returns Array of all siblings.
1655
+ */
1656
+ async getAllSiblings(node) {
1657
+ const filter = node.parentId
1658
+ ? {
1659
+ ...this.getFilter(this._nodes$.value.find((n) => n.id === node.parentId)),
1660
+ parentId: node.parentId,
1661
+ }
1662
+ : { ...this._filter$.value, parentId: undefined };
1663
+ // get all pages of siblings
1664
+ const allSiblings = [];
1665
+ let pageNumber = 1;
1666
+ let hasMore = true;
1667
+ while (hasMore) {
1668
+ const page = await lastValueFrom(this._service.getNodes(filter, pageNumber, this._pageSize, this._hasMockRoot));
1669
+ if (page) {
1670
+ allSiblings.push(...page.items);
1671
+ pageNumber++;
1672
+ hasMore = pageNumber <= page.pageCount;
1673
+ }
1674
+ else {
1675
+ hasMore = false;
1676
+ }
1677
+ }
1678
+ return allSiblings;
1679
+ }
1680
+ /**
1681
+ * Get the ID of the node that should be selected after deleting the
1682
+ * node with the specified ID. This follows the priority:
1683
+ * 1. Next sibling of the deleted node
1684
+ * 2. Previous sibling of the deleted node
1685
+ * 3. Parent node of the deleted node
1686
+ * 4. null if none of the above exist
1687
+ *
1688
+ * @param id The ID of the node to be deleted.
1689
+ * @returns The ID of the anchor node, or null if none can be determined.
1690
+ */
1691
+ getAnchorForDeletedNode(id) {
1692
+ const node = this._nodes$.value.find((n) => n.id === id);
1693
+ if (!node) {
1694
+ return null;
1695
+ }
1696
+ // get all siblings (nodes with same parentId and y level)
1697
+ const siblings = this._nodes$.value.filter((n) => n.parentId === node.parentId && n.y === node.y);
1698
+ // sort siblings by x coordinate
1699
+ siblings.sort((a, b) => a.x - b.x);
1700
+ // find the node's index in the sorted siblings
1701
+ const nodeIndex = siblings.findIndex((n) => n.id === id);
1702
+ if (nodeIndex === -1) {
1703
+ return null;
1704
+ }
1705
+ // 1. Try next sibling
1706
+ if (nodeIndex < siblings.length - 1) {
1707
+ return siblings[nodeIndex + 1].id;
1708
+ }
1709
+ // 2. Try previous sibling
1710
+ if (nodeIndex > 0) {
1711
+ return siblings[nodeIndex - 1].id;
1712
+ }
1713
+ // 3. Try parent
1714
+ if (node.parentId !== undefined) {
1715
+ return node.parentId;
1716
+ }
1717
+ // 4. No anchor found
1718
+ return null;
1719
+ }
1458
1720
  }
1459
1721
 
1460
1722
  /**