@myrmidon/paged-data-browsers 5.0.2 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,10 +12,14 @@ 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
14
  import { BehaviorSubject, of, tap, forkJoin } from 'rxjs';
15
+ import { map } from 'rxjs/operators';
15
16
 
16
17
  class CompactPagerComponent {
17
18
  constructor() {
18
- this.paging = input({ pageNumber: 0, pageCount: 0, total: 0 });
19
+ /**
20
+ * The current paging information.
21
+ */
22
+ this.paging = input({ pageNumber: 0, pageCount: 0, total: 0 }, ...(ngDevMode ? [{ debugName: "paging" }] : []));
19
23
  /**
20
24
  * Emits the new paging information when the user changes the page.
21
25
  */
@@ -48,12 +52,12 @@ class CompactPagerComponent {
48
52
  });
49
53
  }
50
54
  }
51
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: CompactPagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
52
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", 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) {\r\n <div class=\"form-row\">\r\n <span id=\"pages\">{{ paging().pageNumber }}/{{ paging().pageCount }}</span>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"onFirst()\"\r\n [disabled]=\"paging().pageNumber < 2\"\r\n >\r\n <mat-icon>first_page</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"onPrevious()\"\r\n [disabled]=\"paging().pageNumber < 2\"\r\n >\r\n <mat-icon>navigate_before</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"onNext()\"\r\n [disabled]=\"paging().pageNumber === paging().pageCount\"\r\n >\r\n <mat-icon>navigate_next</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"onLast()\"\r\n [disabled]=\"paging().pageNumber === paging().pageCount\"\r\n >\r\n <mat-icon>last_page</mat-icon>\r\n </button>\r\n <span id=\"total\">{{ paging().total }} </span>\r\n </div>\r\n}\r\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: "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"] }] }); }
53
57
  }
54
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: CompactPagerComponent, decorators: [{
58
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: CompactPagerComponent, decorators: [{
55
59
  type: Component,
56
- args: [{ selector: 'pdb-compact-pager', imports: [CommonModule, MatButtonModule, MatIconModule], template: "@if (paging().pageCount) {\r\n <div class=\"form-row\">\r\n <span id=\"pages\">{{ paging().pageNumber }}/{{ paging().pageCount }}</span>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"onFirst()\"\r\n [disabled]=\"paging().pageNumber < 2\"\r\n >\r\n <mat-icon>first_page</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"onPrevious()\"\r\n [disabled]=\"paging().pageNumber < 2\"\r\n >\r\n <mat-icon>navigate_before</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"onNext()\"\r\n [disabled]=\"paging().pageNumber === paging().pageCount\"\r\n >\r\n <mat-icon>navigate_next</mat-icon>\r\n </button>\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n (click)=\"onLast()\"\r\n [disabled]=\"paging().pageNumber === paging().pageCount\"\r\n >\r\n <mat-icon>last_page</mat-icon>\r\n </button>\r\n <span id=\"total\">{{ paging().total }} </span>\r\n </div>\r\n}\r\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"] }]
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"] }]
57
61
  }] });
58
62
 
59
63
  class RangeViewComponent {
@@ -61,19 +65,19 @@ class RangeViewComponent {
61
65
  /**
62
66
  * The domain of the range view (start, limit).
63
67
  */
64
- this.domain = input([0, 100]);
68
+ this.domain = input([0, 100], ...(ngDevMode ? [{ debugName: "domain" }] : []));
65
69
  /**
66
70
  * The range of the range view (start, limit).
67
71
  */
68
- this.range = input([0, 100]);
72
+ this.range = input([0, 100], ...(ngDevMode ? [{ debugName: "range" }] : []));
69
73
  /**
70
74
  * The width of the component.
71
75
  */
72
- this.width = input(100);
76
+ this.width = input(100, ...(ngDevMode ? [{ debugName: "width" }] : []));
73
77
  /**
74
78
  * The height of the component.
75
79
  */
76
- this.height = input(5);
80
+ this.height = input(5, ...(ngDevMode ? [{ debugName: "height" }] : []));
77
81
  this.scaledRange = computed(() => {
78
82
  const domain = this.domain();
79
83
  const range = this.range();
@@ -85,14 +89,14 @@ class RangeViewComponent {
85
89
  (rangeStart / domainWidth) * width,
86
90
  ((rangeStart + rangeWidth) / domainWidth) * width,
87
91
  ];
88
- });
92
+ }, ...(ngDevMode ? [{ debugName: "scaledRange" }] : []));
89
93
  }
90
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: RangeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
91
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.0.3", 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()\">\r\n <rect id=\"rdomain\" [attr.width]=\"width()\" [attr.height]=\"height()\" />\r\n <rect\r\n id=\"rrange\"\r\n [attr.x]=\"scaledRange()[0]\"\r\n [attr.y]=\"0\"\r\n [attr.width]=\"scaledRange()[1] - scaledRange()[0]\"\r\n [attr.height]=\"height()\"\r\n />\r\n</svg>\r\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: "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"] }); }
92
96
  }
93
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: RangeViewComponent, decorators: [{
97
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: RangeViewComponent, decorators: [{
94
98
  type: Component,
95
- args: [{ selector: 'pdb-range-view', template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\r\n <rect id=\"rdomain\" [attr.width]=\"width()\" [attr.height]=\"height()\" />\r\n <rect\r\n id=\"rrange\"\r\n [attr.x]=\"scaledRange()[0]\"\r\n [attr.y]=\"0\"\r\n [attr.width]=\"scaledRange()[1] - scaledRange()[0]\"\r\n [attr.height]=\"height()\"\r\n />\r\n</svg>\r\n", styles: ["#rdomain{fill:#d3d3d3;stroke-width:3;stroke:#c1ba9b}#rrange{fill:#91aad3;stroke-width:3}\n"] }]
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"] }]
96
100
  }], ctorParameters: () => [] });
97
101
 
98
102
  /**
@@ -109,15 +113,15 @@ class BrowserTreeNodeComponent {
109
113
  /**
110
114
  * The node to display.
111
115
  */
112
- this.node = input();
116
+ this.node = input(...(ngDevMode ? [undefined, { debugName: "node" }] : []));
113
117
  /**
114
118
  * The paging information for the node's children.
115
119
  */
116
- this.paging = input();
120
+ this.paging = input(...(ngDevMode ? [undefined, { debugName: "paging" }] : []));
117
121
  /**
118
122
  * True to show debug information.
119
123
  */
120
- this.debug = input();
124
+ this.debug = input(...(ngDevMode ? [undefined, { debugName: "debug" }] : []));
121
125
  /**
122
126
  * True to hide the node's loc and label. This is useful if you want to
123
127
  * provide your own view for the node, between the expansion toggle and
@@ -127,27 +131,27 @@ class BrowserTreeNodeComponent {
127
131
  * the view, then you can just add your own content to this component's
128
132
  * template, without setting this property to true.
129
133
  */
130
- this.hideLabel = input();
134
+ this.hideLabel = input(...(ngDevMode ? [undefined, { debugName: "hideLabel" }] : []));
131
135
  /**
132
136
  * True to hide the node's location (y and x).
133
137
  */
134
- this.hideLoc = input();
138
+ this.hideLoc = input(...(ngDevMode ? [undefined, { debugName: "hideLoc" }] : []));
135
139
  /**
136
140
  * True to hide the node's paging control unless hovered.
137
141
  */
138
- this.hidePaging = input();
142
+ this.hidePaging = input(...(ngDevMode ? [undefined, { debugName: "hidePaging" }] : []));
139
143
  /**
140
144
  * True to hide the node's filter edit button.
141
145
  */
142
- this.hideFilter = input();
146
+ this.hideFilter = input(...(ngDevMode ? [undefined, { debugName: "hideFilter" }] : []));
143
147
  /**
144
148
  * The indent size for the node's children.
145
149
  */
146
- this.indentSize = input(30);
150
+ this.indentSize = input(30, ...(ngDevMode ? [{ debugName: "indentSize" }] : []));
147
151
  /**
148
152
  * The width of the range view.
149
153
  */
150
- this.rangeWidth = input(250);
154
+ this.rangeWidth = input(250, ...(ngDevMode ? [{ debugName: "rangeWidth" }] : []));
151
155
  /**
152
156
  * Emits when the user wants to toggle the expanded state of the node.
153
157
  */
@@ -178,14 +182,14 @@ class BrowserTreeNodeComponent {
178
182
  this.editNodeFilterRequest.emit(this.node());
179
183
  }
180
184
  }
181
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: BrowserTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
182
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", 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()\">\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}#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: "pipe", type:
183
- // ngx-tools
184
- ColorToContrastPipe, name: "colorToContrast" }, { kind: "pipe", type: StringToColorPipe, name: "stringToColor" }, { kind: "component", type:
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
187
  // local
186
- CompactPagerComponent, selector: "pdb-compact-pager", inputs: ["paging"], outputs: ["pagingChange"] }, { kind: "component", type: RangeViewComponent, selector: "pdb-range-view", inputs: ["domain", "range", "width", "height"] }] }); }
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
+ // ngx-tools
190
+ ColorToContrastPipe, name: "colorToContrast" }, { kind: "pipe", type: StringToColorPipe, name: "stringToColor" }] }); }
187
191
  }
188
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: BrowserTreeNodeComponent, decorators: [{
192
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: BrowserTreeNodeComponent, decorators: [{
189
193
  type: Component,
190
194
  args: [{ selector: 'pdb-browser-tree-node', imports: [
191
195
  CommonModule,
@@ -199,7 +203,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImpor
199
203
  // local
200
204
  CompactPagerComponent,
201
205
  RangeViewComponent,
202
- ], template: "@if (node()) {\r\n<div id=\"node\" [style.margin-left.px]=\"(node()!.y - 1) * indentSize()\">\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}#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"] }]
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"] }]
203
207
  }] });
204
208
 
205
209
  /**
@@ -499,11 +503,13 @@ class PagedListStore {
499
503
 
500
504
  /**
501
505
  * A store for the node browser component. This store is used to keep a
502
- * list of nodes, and to load them from the API. It also keeps the root
503
- * node. Every tree node in the list is extended with page number,
504
- * page count and total items, plus expansion-related metadata.
506
+ * list of nodes, and to load them from the injected PagedTreeStoreService<F>.
507
+ * It also keeps the root node(s) in memory. Every tree node in the list
508
+ * is extended with page number, page count and total items, plus
509
+ * expansion-related metadata.
505
510
  * The store keeps a flat list of these tree nodes, allowing users to
506
- * expand and collapse them.
511
+ * expand and collapse them. Each node has its children paged, and optionally
512
+ * filtered.
507
513
  * F is the type of the filter object, E is the type of the paged tree nodes.
508
514
  */
509
515
  class PagedTreeStore {
@@ -539,7 +545,7 @@ class PagedTreeStore {
539
545
  this._hasMockRoot = options.hasMockRoot || false;
540
546
  }
541
547
  /**
542
- * Gets the global filter, eventually overridden with values
548
+ * Gets the global filter, optionally overridden with values
543
549
  * from the specified node's filter.
544
550
  * @param node The optional node.
545
551
  * @returns The filter.
@@ -567,7 +573,8 @@ class PagedTreeStore {
567
573
  return this._nodes$.value;
568
574
  }
569
575
  /**
570
- * Get the root node of the tree.
576
+ * Get the root node of the tree. Given that the store keeps a flat
577
+ * list, this is the first node in the list, if any.
571
578
  * @returns The root node of the tree or undefined if empty.
572
579
  */
573
580
  getRootNode() {
@@ -596,8 +603,11 @@ class PagedTreeStore {
596
603
  * @returns Observable of the page of nodes.
597
604
  */
598
605
  getPageFromCacheOrServer(filter, pageNumber) {
606
+ // try to get the page from cache
599
607
  const key = this.buildCacheKey(pageNumber, filter);
600
608
  const pageInCache = this._cache.get(key);
609
+ // if in cache, just return it; else, get it from the server and
610
+ // store it in cache
601
611
  if (pageInCache) {
602
612
  return of(pageInCache);
603
613
  }
@@ -653,10 +663,11 @@ class PagedTreeStore {
653
663
  this._service
654
664
  .getNodes({
655
665
  ...filter,
656
- parentId: undefined,
666
+ parentId: undefined, // root have no parent
657
667
  }, 1, this._pageSize, this._hasMockRoot)
658
668
  .subscribe({
659
669
  next: (page) => {
670
+ // update the nodes
660
671
  this._nodes$.next(this.createPageNodes(page));
661
672
  // get the children of each node thus calculating their hasChildren property
662
673
  const childrenObservables = this._nodes$.value.map((node) => this.getPageFromCacheOrServer({ ...filter, parentId: node.id }, 1));
@@ -702,13 +713,17 @@ class PagedTreeStore {
702
713
  */
703
714
  expand(id) {
704
715
  return new Promise((resolve, reject) => {
716
+ // get the node to expand
705
717
  const node = this._nodes$.value.find((n) => n.id === id);
718
+ // if no node, or no children, or already expanded, do nothing
706
719
  if (!node || node.hasChildren === false || node.expanded) {
707
720
  resolve(false);
708
721
  }
722
+ // get the first page of children for this node
709
723
  this.getPageFromCacheOrServer({ ...this.getFilter(node), parentId: id }, 1).subscribe((page) => {
710
724
  // no children, set hasChildren to false
711
725
  if (!page.total) {
726
+ // no children, next time we won't try to expand
712
727
  node.hasChildren = false;
713
728
  resolve(false);
714
729
  }
@@ -776,6 +791,11 @@ class PagedTreeStore {
776
791
  }
777
792
  return true;
778
793
  }
794
+ /**
795
+ * Get the children of the node with the specified ID.
796
+ * @param id The ID of the node whose children you want to get.
797
+ * @returns An array of child nodes.
798
+ */
779
799
  getChildren(id) {
780
800
  const node = this._nodes$.value.find((n) => n.id === id);
781
801
  if (!node || node.hasChildren === false) {
@@ -856,7 +876,8 @@ class PagedTreeStore {
856
876
  return Promise.resolve(true);
857
877
  }
858
878
  /**
859
- * Change the page including the node with the specified ID.
879
+ * Change the page including the children of the parent node with
880
+ * the specified ID.
860
881
  * @param parentId The ID of the parent node whose children are inside the page
861
882
  * you want to change.
862
883
  * @param pageNumber The new page number.
@@ -866,45 +887,64 @@ class PagedTreeStore {
866
887
  return new Promise((resolve, reject) => {
867
888
  // get the parent node
868
889
  const parentNode = this._nodes$.value.find((n) => n.id === parentId);
890
+ // if parent not found, do nothing
869
891
  if (!parentNode) {
870
892
  resolve(false);
893
+ return;
894
+ }
895
+ // parent should be expanded
896
+ if (!parentNode.expanded) {
897
+ resolve(false);
898
+ return;
871
899
  }
872
900
  // get the page
873
- this.getPageFromCacheOrServer({ ...this.getFilter(parentNode), parentId }, pageNumber).subscribe((page) => {
874
- // if page is empty do nothing
875
- if (!page.total) {
876
- resolve(false);
877
- }
878
- else {
901
+ this.getPageFromCacheOrServer({ ...this.getFilter(parentNode), parentId }, pageNumber).subscribe({
902
+ next: (page) => {
903
+ // if page is empty, collapse the parent
904
+ if (!page.total) {
905
+ parentNode.hasChildren = false;
906
+ parentNode.expanded = false;
907
+ this._nodes$.next(this._nodes$.value);
908
+ resolve(false);
909
+ return;
910
+ }
879
911
  this._dirty = true;
880
- // remove all the nodes in the same page of node
881
- // with all their descendants
882
912
  const nodes = this._nodes$.value;
883
- const nodeIndex = nodes.indexOf(parentNode) + 1;
884
- const pageNodes = this.createPageNodes(page);
885
- // find the first node of the node's page
886
- let start = nodeIndex;
887
- const oldPageNr = nodes[start].paging.pageNumber;
888
- while (start > 0 &&
889
- nodes[start - 1].parentId === parentId &&
890
- nodes[start - 1].paging.pageNumber === oldPageNr) {
891
- start--;
913
+ const parentIndex = nodes.indexOf(parentNode);
914
+ if (parentIndex === -1) {
915
+ reject(`Parent node ID ${parentId} not found in store`);
916
+ return;
892
917
  }
893
- // find the last node of the node's page,
894
- // including all their descendants
918
+ const pageNodes = this.createPageNodes(page);
919
+ // find the range of children to replace
920
+ let start = parentIndex + 1;
895
921
  let end = start;
896
- const y = nodes[start].y;
897
- while (end < nodes.length && nodes[end].y >= y) {
922
+ // find all current children of this parent (at any page)
923
+ while (end < nodes.length &&
924
+ nodes[end].parentId === parentId &&
925
+ nodes[end].y === parentNode.y + 1) {
926
+ // skip descendants of children
927
+ const childY = nodes[end].y;
898
928
  end++;
929
+ while (end < nodes.length && nodes[end].y > childY) {
930
+ end++;
931
+ }
899
932
  }
900
- // replace all these nodes with the new ones
901
- nodes.splice(start, end - start);
902
- nodes.splice(start, 0, ...pageNodes);
903
- // update the parent node paging info
904
- parentNode.paging.pageNumber = page.pageNumber;
933
+ // replace the children with the new page
934
+ nodes.splice(start, end - start, ...pageNodes);
935
+ // update parent paging info
936
+ parentNode.paging = {
937
+ pageNumber: page.pageNumber,
938
+ pageCount: page.pageCount,
939
+ total: page.total,
940
+ };
905
941
  this._nodes$.next(nodes);
906
942
  resolve(true);
907
- }
943
+ },
944
+ error: (error) => {
945
+ console.error('Error changing page:', error);
946
+ reject(error);
947
+ },
908
948
  });
909
949
  });
910
950
  }
@@ -932,6 +972,731 @@ class PagedTreeStore {
932
972
  const key = this.buildCacheKey(pageNumber, filter);
933
973
  return this._cache.has(key);
934
974
  }
975
+ /**
976
+ * Remove all hilite properties from all nodes in the store.
977
+ */
978
+ removeHilites() {
979
+ const nodes = this._nodes$.value;
980
+ nodes.forEach((node) => {
981
+ node.hilite = undefined;
982
+ });
983
+ this._nodes$.next(nodes);
984
+ }
985
+ /**
986
+ * Find all nodes whose labels include the specified search text,
987
+ * highlight them, and ensure they are visible by expanding their ancestors.
988
+ * @param searchText The text to search for in node labels (case-insensitive).
989
+ * @returns Promise that resolves when all matching nodes are highlighted and visible.
990
+ */
991
+ async findLabels(searchText) {
992
+ if (!searchText || searchText.trim().length === 0) {
993
+ this.removeHilites();
994
+ return;
995
+ }
996
+ // first remove all existing hilites
997
+ this.removeHilites();
998
+ const searchLower = searchText.toLowerCase();
999
+ const matchingNodes = [];
1000
+ // find all matching nodes (including those not currently visible):
1001
+ // search through all possible nodes, not just visible ones
1002
+ await this.searchAllNodes(searchLower, matchingNodes);
1003
+ // highlight the matching nodes
1004
+ matchingNodes.forEach((node) => {
1005
+ node.hilite = true;
1006
+ });
1007
+ // ensure all matching nodes are visible by expanding their ancestor paths
1008
+ const expansionPromises = [];
1009
+ for (const node of matchingNodes) {
1010
+ expansionPromises.push(this.ensureNodeVisible(node.id));
1011
+ }
1012
+ await Promise.all(expansionPromises);
1013
+ // update the nodes observable
1014
+ this._nodes$.next(this._nodes$.value);
1015
+ }
1016
+ /**
1017
+ * Search through all nodes recursively to find matches, including nodes
1018
+ * that are not currently loaded/visible.
1019
+ * @param searchLower The search text in lowercase.
1020
+ * @param matchingNodes Array to collect matching nodes.
1021
+ */
1022
+ async searchAllNodes(searchLower, matchingNodes) {
1023
+ // start with root nodes
1024
+ const rootNodes = this._nodes$.value.filter((n) => n.parentId === undefined);
1025
+ for (const rootNode of rootNodes) {
1026
+ await this.searchNodeAndDescendants(rootNode, searchLower, matchingNodes);
1027
+ }
1028
+ }
1029
+ /**
1030
+ * Recursively search a node and all its descendants for label matches.
1031
+ * @param node The node to search.
1032
+ * @param searchLower The search text in lowercase.
1033
+ * @param matchingNodes Array to collect matching nodes.
1034
+ */
1035
+ async searchNodeAndDescendants(node, searchLower, matchingNodes) {
1036
+ // check if current node matches
1037
+ if (node.label.toLowerCase().includes(searchLower)) {
1038
+ matchingNodes.push(node);
1039
+ }
1040
+ // if node has children, we need to load them to search through them
1041
+ if (node.hasChildren) {
1042
+ // Expand the node if not already expanded to load its children
1043
+ if (!node.expanded) {
1044
+ await this.expand(node.id);
1045
+ }
1046
+ // get all direct children and search them recursively
1047
+ const children = this.getChildren(node.id);
1048
+ for (const child of children) {
1049
+ await this.searchNodeAndDescendants(child, searchLower, matchingNodes);
1050
+ }
1051
+ }
1052
+ }
1053
+ /**
1054
+ * Ensure a node is visible by expanding all its ancestors.
1055
+ * @param nodeId The ID of the node to make visible.
1056
+ */
1057
+ async ensureNodeVisible(nodeId) {
1058
+ const node = this._nodes$.value.find((n) => n.id === nodeId);
1059
+ if (!node) {
1060
+ return;
1061
+ }
1062
+ // find the path from root to this node
1063
+ const ancestorPath = [];
1064
+ let currentNode = node;
1065
+ while (currentNode && currentNode.parentId !== undefined) {
1066
+ ancestorPath.unshift(currentNode.parentId);
1067
+ currentNode = this._nodes$.value.find((n) => n.id === currentNode.parentId);
1068
+ }
1069
+ // expand all ancestors in order
1070
+ for (const ancestorId of ancestorPath) {
1071
+ const ancestor = this._nodes$.value.find((n) => n.id === ancestorId);
1072
+ if (ancestor && ancestor.hasChildren && !ancestor.expanded) {
1073
+ await this.expand(ancestorId);
1074
+ }
1075
+ }
1076
+ }
1077
+ get _isEditable() {
1078
+ // check if the service supports editing
1079
+ return 'saveChanges' in this._service;
1080
+ }
1081
+ get _editableService() {
1082
+ return this._isEditable
1083
+ ? this._service
1084
+ : undefined;
1085
+ }
1086
+ /**
1087
+ * Add a child node to the specified parent.
1088
+ * @param parentId The ID of the parent node.
1089
+ * @param child The child node to add (without ID).
1090
+ * @param first If true, add as first child; otherwise add as last child.
1091
+ * @returns The added node with temporary ID, or undefined if service doesn't support editing.
1092
+ */
1093
+ addChild(parentId, child, first = false) {
1094
+ return new Promise((resolve, reject) => {
1095
+ const editableService = this._editableService;
1096
+ if (!editableService) {
1097
+ resolve(undefined);
1098
+ return;
1099
+ }
1100
+ const parent = this._nodes$.value.find((n) => n.id === parentId);
1101
+ if (!parent) {
1102
+ reject(`Parent node with ID ${parentId} not found`);
1103
+ return;
1104
+ }
1105
+ // calculate position
1106
+ const siblings = this.getChildren(parentId);
1107
+ const position = first ? 1 : siblings.length + 1;
1108
+ const y = parent.y + 1;
1109
+ // adjust x values of existing siblings if inserting at beginning
1110
+ if (first && siblings.length > 0) {
1111
+ siblings.forEach((sibling) => {
1112
+ sibling.x++;
1113
+ editableService.updateNode(sibling.id, { x: sibling.x });
1114
+ });
1115
+ }
1116
+ const newChild = {
1117
+ ...child,
1118
+ parentId,
1119
+ y,
1120
+ x: position,
1121
+ };
1122
+ const addedNode = editableService.addNode(newChild, parentId, position);
1123
+ // update parent hasChildren if it was false
1124
+ if (parent.hasChildren === false) {
1125
+ parent.hasChildren = true;
1126
+ editableService.updateNode(parentId, { hasChildren: true });
1127
+ }
1128
+ // if parent is expanded, add the node to the visible list
1129
+ if (parent.expanded) {
1130
+ const nodes = this._nodes$.value;
1131
+ const parentIndex = nodes.indexOf(parent);
1132
+ if (parentIndex !== -1) {
1133
+ const insertIndex = first
1134
+ ? parentIndex + 1
1135
+ : parentIndex + siblings.length + 1;
1136
+ const pagedNode = {
1137
+ ...addedNode,
1138
+ paging: parent.paging,
1139
+ expanded: false,
1140
+ };
1141
+ nodes.splice(insertIndex, 0, pagedNode);
1142
+ this._nodes$.next(nodes);
1143
+ }
1144
+ }
1145
+ this._dirty = true;
1146
+ this.invalidateCache(parentId);
1147
+ resolve(addedNode);
1148
+ });
1149
+ }
1150
+ /**
1151
+ * Add a sibling node next to the anchor node.
1152
+ * @param anchorId The ID of the anchor node.
1153
+ * @param sibling The sibling node to add (without ID).
1154
+ * @param before If true, add before anchor; otherwise add after anchor.
1155
+ * @returns The added node with temporary ID, or undefined if service doesn't support editing.
1156
+ */
1157
+ addSibling(anchorId, sibling, before = false) {
1158
+ return new Promise((resolve, reject) => {
1159
+ const editableService = this._editableService;
1160
+ if (!editableService) {
1161
+ resolve(undefined);
1162
+ return;
1163
+ }
1164
+ const anchor = this._nodes$.value.find((n) => n.id === anchorId);
1165
+ if (!anchor) {
1166
+ reject(`Anchor node with ID ${anchorId} not found`);
1167
+ return;
1168
+ }
1169
+ // get all siblings of the anchor
1170
+ const siblings = this._nodes$.value.filter((n) => n.parentId === anchor.parentId && n.y === anchor.y);
1171
+ siblings.sort((a, b) => a.x - b.x);
1172
+ const anchorIndex = siblings.findIndex((s) => s.id === anchorId);
1173
+ const insertPosition = before ? anchor.x : anchor.x + 1;
1174
+ // adjust x values of siblings that come after the insertion point
1175
+ siblings.slice(before ? anchorIndex : anchorIndex + 1).forEach((s) => {
1176
+ s.x++;
1177
+ editableService.updateNode(s.id, { x: s.x });
1178
+ });
1179
+ const newSibling = {
1180
+ ...sibling,
1181
+ parentId: anchor.parentId,
1182
+ y: anchor.y,
1183
+ x: insertPosition,
1184
+ };
1185
+ const addedNode = editableService.addNode(newSibling, anchor.parentId);
1186
+ // add to visible list if the parent is expanded
1187
+ const nodes = this._nodes$.value;
1188
+ const anchorVisibleIndex = nodes.indexOf(anchor);
1189
+ if (anchorVisibleIndex !== -1) {
1190
+ const insertIndex = before
1191
+ ? anchorVisibleIndex
1192
+ : anchorVisibleIndex + 1;
1193
+ const pagedNode = {
1194
+ ...addedNode,
1195
+ paging: anchor.paging,
1196
+ expanded: false,
1197
+ };
1198
+ nodes.splice(insertIndex, 0, pagedNode);
1199
+ this._nodes$.next(nodes);
1200
+ }
1201
+ this._dirty = true;
1202
+ this.invalidateCache(anchor.parentId);
1203
+ resolve(addedNode);
1204
+ });
1205
+ }
1206
+ /**
1207
+ * Remove a node from the tree.
1208
+ * We only track the deletion and update visible siblings.
1209
+ * Coordinate adjustments for non-visible nodes are handled by applyLocalChanges.
1210
+ */
1211
+ removeNode(nodeId) {
1212
+ return new Promise((resolve, reject) => {
1213
+ const editableService = this._editableService;
1214
+ if (!editableService) {
1215
+ resolve(false);
1216
+ return;
1217
+ }
1218
+ const node = this._nodes$.value.find((n) => n.id === nodeId);
1219
+ if (!node) {
1220
+ resolve(false);
1221
+ return;
1222
+ }
1223
+ const parentId = node.parentId;
1224
+ const nodeY = node.y;
1225
+ const nodeX = node.x;
1226
+ // CRITICAL: ensure the node is in the editable service's cache
1227
+ // before removing it, so its original data is preserved
1228
+ if (!editableService.getNode(nodeId)) {
1229
+ // pass the complete node including any extended properties (like 'key'):
1230
+ // this preserves all data needed for persistence
1231
+ editableService.updateNode(nodeId, node);
1232
+ console.log('Added node to cache before deletion:', nodeId);
1233
+ }
1234
+ // remove all descendants first
1235
+ const descendants = this.getDescendants(nodeId);
1236
+ descendants.forEach((descendant) => {
1237
+ // ensure each descendant is in cache before removing
1238
+ if (!editableService.getNode(descendant.id)) {
1239
+ editableService.updateNode(descendant.id, descendant);
1240
+ console.log('Added descendant to cache before deletion:', descendant.id);
1241
+ }
1242
+ editableService.removeNode(descendant.id);
1243
+ });
1244
+ // remove the node itself
1245
+ console.log('Removing node from editable service:', nodeId);
1246
+ editableService.removeNode(nodeId);
1247
+ // update ONLY visible siblings' x coordinates
1248
+ // (Non-visible siblings will get adjusted by applyLocalChanges)
1249
+ const visibleSiblings = this._nodes$.value.filter((n) => n.parentId === parentId && n.y === nodeY && n.x > nodeX);
1250
+ visibleSiblings.forEach((sibling) => {
1251
+ sibling.x--;
1252
+ // this will succeed because these nodes ARE in the local cache
1253
+ editableService.updateNode(sibling.id, { x: sibling.x });
1254
+ });
1255
+ // remove from visible list
1256
+ const nodes = this._nodes$.value;
1257
+ const nodeIndex = nodes.indexOf(node);
1258
+ if (nodeIndex !== -1) {
1259
+ let endIndex = nodeIndex + 1;
1260
+ while (endIndex < nodes.length && nodes[endIndex].y > nodeY) {
1261
+ endIndex++;
1262
+ }
1263
+ nodes.splice(nodeIndex, endIndex - nodeIndex);
1264
+ }
1265
+ // update parent hasChildren if needed
1266
+ if (parentId !== undefined) {
1267
+ const parent = this._nodes$.value.find((n) => n.id === parentId);
1268
+ if (parent && parent.paging.total === 1) {
1269
+ // this was the last child
1270
+ parent.hasChildren = false;
1271
+ parent.expanded = false;
1272
+ editableService.updateNode(parentId, { hasChildren: false });
1273
+ this._nodes$.next(nodes);
1274
+ this._dirty = true;
1275
+ this.invalidateCache(parentId);
1276
+ resolve(true);
1277
+ return;
1278
+ }
1279
+ }
1280
+ this._nodes$.next(nodes);
1281
+ this._dirty = true;
1282
+ 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
+ }
1293
+ resolve(true);
1294
+ });
1295
+ }
1296
+ /**
1297
+ * Replace a node with a new one.
1298
+ * @param oldNodeId The ID of the node to replace.
1299
+ * @param newNode The new node data (without ID).
1300
+ * @param keepDescendants If true, keep descendants; otherwise remove them.
1301
+ * @returns Promise that resolves to the new node, or undefined if service
1302
+ * doesn't support editing.
1303
+ */
1304
+ replaceNode(oldNodeId, newNode, keepDescendants = true) {
1305
+ return new Promise((resolve, reject) => {
1306
+ const editableService = this._editableService;
1307
+ if (!editableService) {
1308
+ resolve(undefined);
1309
+ return;
1310
+ }
1311
+ const oldNode = this._nodes$.value.find((n) => n.id === oldNodeId);
1312
+ if (!oldNode) {
1313
+ reject(`Node with ID ${oldNodeId} not found`);
1314
+ return;
1315
+ }
1316
+ const updatedNode = {
1317
+ ...newNode,
1318
+ id: oldNodeId,
1319
+ parentId: oldNode.parentId,
1320
+ y: oldNode.y,
1321
+ x: oldNode.x,
1322
+ };
1323
+ editableService.updateNode(oldNodeId, updatedNode);
1324
+ // handle descendants
1325
+ if (!keepDescendants) {
1326
+ const descendants = this.getDescendants(oldNodeId);
1327
+ descendants.forEach((descendant) => {
1328
+ editableService.removeNode(descendant.id);
1329
+ });
1330
+ // update hasChildren
1331
+ editableService.updateNode(oldNodeId, { hasChildren: false });
1332
+ }
1333
+ // update in visible list
1334
+ const nodes = this._nodes$.value;
1335
+ const nodeIndex = nodes.findIndex((n) => n.id === oldNodeId);
1336
+ if (nodeIndex !== -1) {
1337
+ const pagedNode = nodes[nodeIndex];
1338
+ Object.assign(pagedNode, updatedNode);
1339
+ if (!keepDescendants) {
1340
+ pagedNode.hasChildren = false;
1341
+ pagedNode.expanded = false;
1342
+ // remove descendants from visible list
1343
+ let endIndex = nodeIndex + 1;
1344
+ while (endIndex < nodes.length && nodes[endIndex].y > oldNode.y) {
1345
+ endIndex++;
1346
+ }
1347
+ if (endIndex > nodeIndex + 1) {
1348
+ nodes.splice(nodeIndex + 1, endIndex - nodeIndex - 1);
1349
+ }
1350
+ }
1351
+ this._nodes$.next(nodes);
1352
+ }
1353
+ this._dirty = true;
1354
+ this.invalidateCache(oldNode.parentId);
1355
+ resolve(updatedNode);
1356
+ });
1357
+ }
1358
+ /**
1359
+ * Get all descendants of a node.
1360
+ */
1361
+ getDescendants(nodeId) {
1362
+ const node = this._nodes$.value.find((n) => n.id === nodeId);
1363
+ if (!node)
1364
+ return [];
1365
+ const descendants = [];
1366
+ const nodes = this._nodes$.value;
1367
+ const nodeIndex = nodes.indexOf(node);
1368
+ if (nodeIndex !== -1) {
1369
+ let i = nodeIndex + 1;
1370
+ while (i < nodes.length && nodes[i].y > node.y) {
1371
+ descendants.push(nodes[i]);
1372
+ i++;
1373
+ }
1374
+ }
1375
+ return descendants;
1376
+ }
1377
+ /**
1378
+ * Invalidate cache for a specific parent and its ancestors.
1379
+ */
1380
+ invalidateCache(parentId) {
1381
+ // clear cache entries that might be affected by the change
1382
+ this._cache.clear();
1383
+ // if we have a specific parent, we could be more selective about
1384
+ // cache invalidation but for now, clearing all is safer
1385
+ }
1386
+ /**
1387
+ * Save all pending changes if the service supports it.
1388
+ * Preserves the current expansion state after saving.
1389
+ * @returns Promise that resolves when save is complete,
1390
+ * with ID mappings for new nodes.
1391
+ */
1392
+ saveChanges() {
1393
+ return new Promise((resolve, reject) => {
1394
+ const editableService = this._editableService;
1395
+ if (!editableService) {
1396
+ resolve(undefined);
1397
+ return;
1398
+ }
1399
+ // capture current expansion state before saving
1400
+ const expandedNodeIds = this._nodes$.value
1401
+ .filter((n) => n.expanded)
1402
+ .map((n) => n.id);
1403
+ editableService.saveChanges().subscribe({
1404
+ next: (idMap) => {
1405
+ // update temporary IDs to permanent IDs in the visible list
1406
+ if (idMap.size > 0) {
1407
+ const nodes = this._nodes$.value;
1408
+ // update node IDs
1409
+ nodes.forEach((node) => {
1410
+ const newId = idMap.get(node.id);
1411
+ if (newId !== undefined) {
1412
+ node.id = newId;
1413
+ }
1414
+ });
1415
+ // update parent IDs for children of replaced nodes
1416
+ nodes.forEach((node) => {
1417
+ if (node.parentId !== undefined) {
1418
+ const newParentId = idMap.get(node.parentId);
1419
+ if (newParentId !== undefined) {
1420
+ node.parentId = newParentId;
1421
+ }
1422
+ }
1423
+ });
1424
+ this._nodes$.next(nodes);
1425
+ }
1426
+ // clear cache to force fresh data
1427
+ this._cache.clear();
1428
+ this._dirty = false;
1429
+ // restore expansion state by re-expanding previously expanded nodes
1430
+ const restoreExpansion = async () => {
1431
+ for (const oldId of expandedNodeIds) {
1432
+ const newId = idMap.get(oldId) ?? oldId;
1433
+ const node = this._nodes$.value.find((n) => n.id === newId);
1434
+ if (node && !node.expanded) {
1435
+ await this.expand(newId);
1436
+ }
1437
+ }
1438
+ };
1439
+ restoreExpansion().then(() => resolve(idMap));
1440
+ },
1441
+ error: (error) => reject(error),
1442
+ });
1443
+ });
1444
+ }
1445
+ /**
1446
+ * Check if there are unsaved changes.
1447
+ */
1448
+ hasUnsavedChanges() {
1449
+ return this._editableService?.hasChanges() || false;
1450
+ }
1451
+ /**
1452
+ * Clear all unsaved changes.
1453
+ */
1454
+ clearUnsavedChanges() {
1455
+ this._editableService?.clearChanges();
1456
+ this._dirty = true;
1457
+ }
1458
+ }
1459
+
1460
+ /**
1461
+ * Types of operations that can be performed on tree nodes.
1462
+ */
1463
+ var ChangeOperationType;
1464
+ (function (ChangeOperationType) {
1465
+ ChangeOperationType["ADD"] = "add";
1466
+ ChangeOperationType["REMOVE"] = "remove";
1467
+ ChangeOperationType["UPDATE"] = "update";
1468
+ })(ChangeOperationType || (ChangeOperationType = {}));
1469
+ /**
1470
+ * Base implementation of EditablePagedTreeStoreService that handles change tracking
1471
+ * and provides common editing functionality. Implementers only need to override
1472
+ * the abstract methods for actual data persistence.
1473
+ */
1474
+ class EditablePagedTreeStoreServiceBase {
1475
+ constructor() {
1476
+ this._changes = [];
1477
+ this._nextTempId = -1;
1478
+ this._nodes = new Map();
1479
+ this._removedNodes = new Set();
1480
+ this._hasChanges$ = new BehaviorSubject(false);
1481
+ /**
1482
+ * Observable that emits when the change state changes.
1483
+ */
1484
+ this.hasChanges$ = this._hasChanges$.asObservable();
1485
+ }
1486
+ /**
1487
+ * Get nodes, including any local changes that haven't been saved yet.
1488
+ */
1489
+ getNodes(filter, pageNumber, pageSize, hasMockRoot) {
1490
+ return this.fetchNodes(filter, pageNumber, pageSize, hasMockRoot).pipe(map((page) => this.applyLocalChanges(page, filter)));
1491
+ }
1492
+ /**
1493
+ * Apply local changes to a page of nodes.
1494
+ * This includes applying x-coordinate adjustments for siblings of deleted nodes.
1495
+ */
1496
+ applyLocalChanges(page, filter) {
1497
+ let items = [...page.items];
1498
+ // Step 1: Remove deleted nodes from the current page
1499
+ items = items.filter((node) => !this._removedNodes.has(node.id));
1500
+ // Step 2: Calculate x-coordinate adjustments for this page based on deletions
1501
+ // For each removed node that would affect this page, adjust x coordinates
1502
+ const xAdjustments = new Map(); // nodeId -> adjustment amount
1503
+ this._removedNodes.forEach((removedId) => {
1504
+ const removeChange = this._changes.find((c) => c.type === ChangeOperationType.REMOVE && c.id === removedId);
1505
+ if (removeChange?.originalNode) {
1506
+ const removedNode = removeChange.originalNode;
1507
+ // For each item in current page that was a sibling after the removed node
1508
+ items.forEach((item) => {
1509
+ if (item.parentId === removedNode.parentId &&
1510
+ item.y === removedNode.y &&
1511
+ item.x > removedNode.x) {
1512
+ // This sibling should have its x decreased
1513
+ const currentAdjustment = xAdjustments.get(item.id) || 0;
1514
+ xAdjustments.set(item.id, currentAdjustment - 1);
1515
+ }
1516
+ });
1517
+ }
1518
+ });
1519
+ // Step 3: Apply updates to existing nodes, including coordinate adjustments
1520
+ items = items.map((node) => {
1521
+ let result = { ...node };
1522
+ // First apply any explicit updates from cache
1523
+ const updated = this._nodes.get(node.id);
1524
+ if (updated && !this._removedNodes.has(node.id)) {
1525
+ result = { ...result, ...updated };
1526
+ }
1527
+ // Then apply calculated x-coordinate adjustments from deletions
1528
+ const xAdjustment = xAdjustments.get(node.id);
1529
+ if (xAdjustment !== undefined && xAdjustment !== 0) {
1530
+ result.x = result.x + xAdjustment;
1531
+ }
1532
+ return result;
1533
+ });
1534
+ // Step 4: Add new nodes that belong to this specific page
1535
+ const pageParentId = filter.parentId;
1536
+ const newNodesForThisPage = Array.from(this._nodes.values()).filter((node) => node.id < 0 && // temporary ID (newly added)
1537
+ !this._removedNodes.has(node.id) && // not removed
1538
+ node.parentId === pageParentId && // belongs to the same parent
1539
+ this.matchesFilter(node, filter) && // matches the filter
1540
+ node.label !== undefined && // has all required properties
1541
+ node.x !== undefined &&
1542
+ node.y !== undefined);
1543
+ items.push(...newNodesForThisPage);
1544
+ // Step 5: Sort items by their position (x coordinate)
1545
+ items.sort((a, b) => a.x - b.x);
1546
+ // Step 6: Adjust total count to reflect deletions and additions for this parent
1547
+ let totalAdjustment = 0;
1548
+ // Count deletions that affect this parent
1549
+ this._removedNodes.forEach((removedId) => {
1550
+ const change = this._changes.find((c) => c.type === ChangeOperationType.REMOVE && c.id === removedId);
1551
+ if (change?.originalNode?.parentId === pageParentId) {
1552
+ totalAdjustment--;
1553
+ }
1554
+ });
1555
+ // Count additions for this parent
1556
+ totalAdjustment += newNodesForThisPage.length;
1557
+ const adjustedTotal = Math.max(0, page.total + totalAdjustment);
1558
+ const adjustedPageCount = Math.ceil(adjustedTotal / (page.pageSize || 20));
1559
+ return {
1560
+ ...page,
1561
+ items: items,
1562
+ total: adjustedTotal,
1563
+ pageCount: adjustedPageCount,
1564
+ };
1565
+ }
1566
+ /**
1567
+ * Check if a node matches the given filter.
1568
+ */
1569
+ matchesFilter(node, filter) {
1570
+ if (filter.parentId !== undefined && node.parentId !== filter.parentId) {
1571
+ return false;
1572
+ }
1573
+ if (filter.tags &&
1574
+ filter.tags.length > 0 &&
1575
+ (!node.tag || !filter.tags.includes(node.tag))) {
1576
+ return false;
1577
+ }
1578
+ return true;
1579
+ }
1580
+ /**
1581
+ * Add a new node to the tree.
1582
+ */
1583
+ addNode(node, parentId, position) {
1584
+ const newNode = {
1585
+ ...node,
1586
+ id: this._nextTempId--,
1587
+ parentId,
1588
+ };
1589
+ this._nodes.set(newNode.id, newNode);
1590
+ this._changes.push({
1591
+ type: ChangeOperationType.ADD,
1592
+ id: newNode.id,
1593
+ node: newNode,
1594
+ parentId,
1595
+ position,
1596
+ });
1597
+ this.updateHasChanges();
1598
+ return newNode;
1599
+ }
1600
+ /**
1601
+ * Remove a node from the tree.
1602
+ */
1603
+ removeNode(id) {
1604
+ const node = this._nodes.get(id);
1605
+ if (node || !this._removedNodes.has(id)) {
1606
+ this._removedNodes.add(id);
1607
+ this._changes.push({
1608
+ type: ChangeOperationType.REMOVE,
1609
+ id,
1610
+ originalNode: node,
1611
+ });
1612
+ this.updateHasChanges();
1613
+ }
1614
+ }
1615
+ /**
1616
+ * Update an existing node.
1617
+ * If the node is in cache, update it with the partial updates.
1618
+ * If not in cache and updates is a complete TreeNode, add it to cache.
1619
+ * Otherwise skip (normal for pagination).
1620
+ */
1621
+ updateNode(id, updates) {
1622
+ const existing = this._nodes.get(id);
1623
+ if (!existing) {
1624
+ // Check if updates contains a complete TreeNode (used for deletion prep)
1625
+ const isCompleteNode = updates.label !== undefined &&
1626
+ updates.y !== undefined &&
1627
+ updates.x !== undefined;
1628
+ if (isCompleteNode) {
1629
+ // Add the complete node to cache (used before deletion)
1630
+ const completeNode = { ...updates, id };
1631
+ this._nodes.set(id, completeNode);
1632
+ // Don't add a change operation - this is just caching for later operations
1633
+ }
1634
+ // Otherwise silently skip - normal for pagination
1635
+ return;
1636
+ }
1637
+ const updated = { ...existing, ...updates, id };
1638
+ this._nodes.set(id, updated);
1639
+ this._changes.push({
1640
+ type: ChangeOperationType.UPDATE,
1641
+ id,
1642
+ node: updated,
1643
+ originalNode: existing,
1644
+ });
1645
+ this.updateHasChanges();
1646
+ }
1647
+ /**
1648
+ * Save all pending changes.
1649
+ */
1650
+ saveChanges() {
1651
+ if (this._changes.length === 0) {
1652
+ return of(new Map());
1653
+ }
1654
+ return this.persistChanges([...this._changes]).pipe(map((idMap) => {
1655
+ // update temporary IDs with permanent ones
1656
+ idMap.forEach((permanentId, tempId) => {
1657
+ const node = this._nodes.get(tempId);
1658
+ if (node) {
1659
+ this._nodes.delete(tempId);
1660
+ this._nodes.set(permanentId, { ...node, id: permanentId });
1661
+ }
1662
+ });
1663
+ // clear changes after successful save
1664
+ this._changes = [];
1665
+ this._removedNodes.clear();
1666
+ this.updateHasChanges();
1667
+ return idMap;
1668
+ }));
1669
+ }
1670
+ /**
1671
+ * Check if there are unsaved changes.
1672
+ */
1673
+ hasChanges() {
1674
+ return this._changes.length > 0;
1675
+ }
1676
+ /**
1677
+ * Clear all pending changes.
1678
+ */
1679
+ clearChanges() {
1680
+ this._changes = [];
1681
+ this._nodes.clear();
1682
+ this._removedNodes.clear();
1683
+ this.updateHasChanges();
1684
+ }
1685
+ /**
1686
+ * Get all pending changes.
1687
+ */
1688
+ getChanges() {
1689
+ return [...this._changes];
1690
+ }
1691
+ /**
1692
+ * Get a node by ID, including local changes.
1693
+ */
1694
+ getNode(id) {
1695
+ return this._nodes.get(id);
1696
+ }
1697
+ updateHasChanges() {
1698
+ this._hasChanges$.next(this._changes.length > 0);
1699
+ }
935
1700
  }
936
1701
 
937
1702
  /*
@@ -942,5 +1707,5 @@ class PagedTreeStore {
942
1707
  * Generated bundle index. Do not edit.
943
1708
  */
944
1709
 
945
- export { BrowserTreeNodeComponent, CompactPagerComponent, DEFAULT_PAGED_LIST_STORE_OPTIONS, LRUCache, PagedListStore, PagedTreeStore, RangeViewComponent };
1710
+ export { BrowserTreeNodeComponent, ChangeOperationType, CompactPagerComponent, DEFAULT_PAGED_LIST_STORE_OPTIONS, EditablePagedTreeStoreServiceBase, LRUCache, PagedListStore, PagedTreeStore, RangeViewComponent };
946
1711
  //# sourceMappingURL=myrmidon-paged-data-browsers.mjs.map