@myrmidon/paged-data-browsers 5.0.3 → 5.1.2

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,9 +12,13 @@ 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() {
19
+ /**
20
+ * The current paging information.
21
+ */
18
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.
@@ -48,13 +52,13 @@ class CompactPagerComponent {
48
52
  });
49
53
  }
50
54
  }
51
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: CompactPagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
52
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.1", type: CompactPagerComponent, isStandalone: true, selector: "pdb-compact-pager", inputs: { paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pagingChange: "pagingChange" }, ngImport: i0, template: "@if (paging().pageCount) {\n <div class=\"form-row\">\n <span id=\"pages\">{{ paging().pageNumber }}/{{ paging().pageCount }}</span>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onFirst()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>first_page</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onPrevious()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>navigate_before</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onNext()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>navigate_next</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onLast()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>last_page</mat-icon>\n </button>\n <span id=\"total\">{{ paging().total }} </span>\n </div>\n}\n", styles: ["#pages,#total{color:silver}.form-row{display:flex;gap:2px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] }); }
55
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CompactPagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
56
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.0", 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.3.1", ngImport: i0, type: CompactPagerComponent, decorators: [{
58
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: CompactPagerComponent, decorators: [{
55
59
  type: Component,
56
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
+ }], propDecorators: { paging: [{ type: i0.Input, args: [{ isSignal: true, alias: "paging", required: false }] }], pagingChange: [{ type: i0.Output, args: ["pagingChange"] }] } });
58
62
 
59
63
  class RangeViewComponent {
60
64
  constructor() {
@@ -87,13 +91,13 @@ class RangeViewComponent {
87
91
  ];
88
92
  }, ...(ngDevMode ? [{ debugName: "scaledRange" }] : []));
89
93
  }
90
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: RangeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
91
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.1", type: RangeViewComponent, isStandalone: true, selector: "pdb-range-view", inputs: { domain: { classPropertyName: "domain", publicName: "domain", isSignal: true, isRequired: false, transformFunction: null }, range: { classPropertyName: "range", publicName: "range", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <rect id=\"rdomain\" [attr.width]=\"width()\" [attr.height]=\"height()\" />\n <rect\n id=\"rrange\"\n [attr.x]=\"scaledRange()[0]\"\n [attr.y]=\"0\"\n [attr.width]=\"scaledRange()[1] - scaledRange()[0]\"\n [attr.height]=\"height()\"\n />\n</svg>\n", styles: ["#rdomain{fill:#d3d3d3;stroke-width:3;stroke:#c1ba9b}#rrange{fill:#91aad3;stroke-width:3}\n"] }); }
94
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: RangeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
95
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.0", 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.3.1", ngImport: i0, type: RangeViewComponent, decorators: [{
97
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: RangeViewComponent, decorators: [{
94
98
  type: Component,
95
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
- }], 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 }] }] } });
97
101
 
98
102
  /**
99
103
  * Browser tree node component view. This wraps some HTML content providing
@@ -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.3.1", ngImport: i0, type: BrowserTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
182
- 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()) {\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: "component", type:
185
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: BrowserTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
186
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.0", 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: "component", type:
183
187
  // local
184
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:
185
189
  // ngx-tools
186
190
  ColorToContrastPipe, name: "colorToContrast" }, { kind: "pipe", type: StringToColorPipe, name: "stringToColor" }] }); }
187
191
  }
188
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: BrowserTreeNodeComponent, decorators: [{
192
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.0", ngImport: i0, type: BrowserTreeNodeComponent, decorators: [{
189
193
  type: Component,
190
194
  args: [{ selector: 'pdb-browser-tree-node', imports: [
191
195
  CommonModule,
@@ -200,7 +204,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImpor
200
204
  CompactPagerComponent,
201
205
  RangeViewComponent,
202
206
  ], 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"] }]
203
- }] });
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"] }] } });
204
208
 
205
209
  /**
206
210
  * A Least Recently Used cache that can be used to store any type of object.
@@ -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
  }
@@ -953,24 +993,24 @@ class PagedTreeStore {
953
993
  this.removeHilites();
954
994
  return;
955
995
  }
956
- // First remove all existing hilites
996
+ // first remove all existing hilites
957
997
  this.removeHilites();
958
998
  const searchLower = searchText.toLowerCase();
959
999
  const matchingNodes = [];
960
- // Find all matching nodes (including those not currently visible)
961
- // We need to search through all possible nodes, not just visible ones
1000
+ // find all matching nodes (including those not currently visible):
1001
+ // search through all possible nodes, not just visible ones
962
1002
  await this.searchAllNodes(searchLower, matchingNodes);
963
- // Highlight the matching nodes
1003
+ // highlight the matching nodes
964
1004
  matchingNodes.forEach((node) => {
965
1005
  node.hilite = true;
966
1006
  });
967
- // Ensure all matching nodes are visible by expanding their ancestor paths
1007
+ // ensure all matching nodes are visible by expanding their ancestor paths
968
1008
  const expansionPromises = [];
969
1009
  for (const node of matchingNodes) {
970
1010
  expansionPromises.push(this.ensureNodeVisible(node.id));
971
1011
  }
972
1012
  await Promise.all(expansionPromises);
973
- // Update the nodes observable
1013
+ // update the nodes observable
974
1014
  this._nodes$.next(this._nodes$.value);
975
1015
  }
976
1016
  /**
@@ -980,7 +1020,7 @@ class PagedTreeStore {
980
1020
  * @param matchingNodes Array to collect matching nodes.
981
1021
  */
982
1022
  async searchAllNodes(searchLower, matchingNodes) {
983
- // Start with root nodes
1023
+ // start with root nodes
984
1024
  const rootNodes = this._nodes$.value.filter((n) => n.parentId === undefined);
985
1025
  for (const rootNode of rootNodes) {
986
1026
  await this.searchNodeAndDescendants(rootNode, searchLower, matchingNodes);
@@ -993,17 +1033,17 @@ class PagedTreeStore {
993
1033
  * @param matchingNodes Array to collect matching nodes.
994
1034
  */
995
1035
  async searchNodeAndDescendants(node, searchLower, matchingNodes) {
996
- // Check if current node matches
1036
+ // check if current node matches
997
1037
  if (node.label.toLowerCase().includes(searchLower)) {
998
1038
  matchingNodes.push(node);
999
1039
  }
1000
- // If node has children, we need to load them to search through them
1040
+ // if node has children, we need to load them to search through them
1001
1041
  if (node.hasChildren) {
1002
1042
  // Expand the node if not already expanded to load its children
1003
1043
  if (!node.expanded) {
1004
1044
  await this.expand(node.id);
1005
1045
  }
1006
- // Get all direct children and search them recursively
1046
+ // get all direct children and search them recursively
1007
1047
  const children = this.getChildren(node.id);
1008
1048
  for (const child of children) {
1009
1049
  await this.searchNodeAndDescendants(child, searchLower, matchingNodes);
@@ -1019,14 +1059,14 @@ class PagedTreeStore {
1019
1059
  if (!node) {
1020
1060
  return;
1021
1061
  }
1022
- // Find the path from root to this node
1062
+ // find the path from root to this node
1023
1063
  const ancestorPath = [];
1024
1064
  let currentNode = node;
1025
1065
  while (currentNode && currentNode.parentId !== undefined) {
1026
1066
  ancestorPath.unshift(currentNode.parentId);
1027
1067
  currentNode = this._nodes$.value.find((n) => n.id === currentNode.parentId);
1028
1068
  }
1029
- // Expand all ancestors in order
1069
+ // expand all ancestors in order
1030
1070
  for (const ancestorId of ancestorPath) {
1031
1071
  const ancestor = this._nodes$.value.find((n) => n.id === ancestorId);
1032
1072
  if (ancestor && ancestor.hasChildren && !ancestor.expanded) {
@@ -1034,6 +1074,629 @@ class PagedTreeStore {
1034
1074
  }
1035
1075
  }
1036
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
+ }
1037
1700
  }
1038
1701
 
1039
1702
  /*
@@ -1044,5 +1707,5 @@ class PagedTreeStore {
1044
1707
  * Generated bundle index. Do not edit.
1045
1708
  */
1046
1709
 
1047
- 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 };
1048
1711
  //# sourceMappingURL=myrmidon-paged-data-browsers.mjs.map