@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: "
|
|
56
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "
|
|
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: "
|
|
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: "
|
|
95
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "
|
|
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: "
|
|
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: "
|
|
186
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "
|
|
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: "
|
|
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.
|
|
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
|
|
1055
|
-
* @param nodeId The ID of the node
|
|
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
|
|
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
|
|
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
|
-
//
|
|
1284
|
-
|
|
1285
|
-
|
|
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
|
/**
|