@myrmidon/paged-data-browsers 0.0.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.
@@ -0,0 +1,902 @@
1
+ import * as i0 from '@angular/core';
2
+ import { EventEmitter, Component, Input, Output, NgModule } from '@angular/core';
3
+ import * as i1 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import * as i2 from '@angular/material/button';
6
+ import { MatButtonModule } from '@angular/material/button';
7
+ import * as i3 from '@angular/material/icon';
8
+ import { MatIconModule } from '@angular/material/icon';
9
+ import * as i2$1 from '@angular/material/badge';
10
+ import { MatBadgeModule } from '@angular/material/badge';
11
+ import * as i5 from '@angular/material/tooltip';
12
+ import { MatTooltipModule } from '@angular/material/tooltip';
13
+ import * as i8 from '@myrmidon/ng-tools';
14
+ import { NgToolsModule } from '@myrmidon/ng-tools';
15
+ import { BehaviorSubject, of, tap, switchMap, forkJoin } from 'rxjs';
16
+ import { ReactiveFormsModule } from '@angular/forms';
17
+ import { MatChipsModule } from '@angular/material/chips';
18
+ import { MatDialogModule } from '@angular/material/dialog';
19
+ import { MatFormFieldModule } from '@angular/material/form-field';
20
+ import { MatInputModule } from '@angular/material/input';
21
+ import { MatPaginatorModule } from '@angular/material/paginator';
22
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
23
+ import { MatSelectModule } from '@angular/material/select';
24
+
25
+ class CompactPagerComponent {
26
+ constructor() {
27
+ this.paging = {
28
+ pageNumber: 0,
29
+ pageCount: 0,
30
+ total: 0,
31
+ };
32
+ this.pagingChange = new EventEmitter();
33
+ }
34
+ onFirst() {
35
+ this.paging = { ...this.paging, pageNumber: 1 };
36
+ this.pagingChange.emit(this.paging);
37
+ }
38
+ onPrevious() {
39
+ if (this.paging.pageNumber > 1) {
40
+ this.paging = { ...this.paging, pageNumber: this.paging.pageNumber - 1 };
41
+ this.pagingChange.emit(this.paging);
42
+ }
43
+ }
44
+ onNext() {
45
+ if (this.paging.pageNumber < this.paging.pageCount) {
46
+ this.paging = { ...this.paging, pageNumber: this.paging.pageNumber + 1 };
47
+ this.pagingChange.emit(this.paging);
48
+ }
49
+ }
50
+ onLast() {
51
+ if (this.paging.pageNumber < this.paging.pageCount) {
52
+ this.paging = { ...this.paging, pageNumber: this.paging.pageCount };
53
+ this.pagingChange.emit(this.paging);
54
+ }
55
+ }
56
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: CompactPagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
57
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.7", type: CompactPagerComponent, selector: "pdb-compact-pager", inputs: { paging: "paging" }, outputs: { pagingChange: "pagingChange" }, ngImport: i0, template: "<div class=\"form-row\" *ngIf=\"paging.pageCount\">\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", 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: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] }); }
58
+ }
59
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: CompactPagerComponent, decorators: [{
60
+ type: Component,
61
+ args: [{ selector: 'pdb-compact-pager', template: "<div class=\"form-row\" *ngIf=\"paging.pageCount\">\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", styles: ["#pages,#total{color:silver}.form-row{display:flex;gap:2px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"] }]
62
+ }], ctorParameters: function () { return []; }, propDecorators: { paging: [{
63
+ type: Input
64
+ }], pagingChange: [{
65
+ type: Output
66
+ }] } });
67
+
68
+ class RangeViewComponent {
69
+ constructor() {
70
+ this.domain = [0, 100];
71
+ this.range = [0, 100];
72
+ this.width = 100;
73
+ this.height = 5;
74
+ this.scaledRange = [];
75
+ }
76
+ ngOnChanges() {
77
+ const domainWidth = this.domain[1] - this.domain[0];
78
+ const rangeWidth = this.range[1] - this.range[0];
79
+ const rangeStart = this.range[0] - this.domain[0];
80
+ this.scaledRange = [
81
+ (rangeStart / domainWidth) * this.width,
82
+ ((rangeStart + rangeWidth) / domainWidth) * this.width,
83
+ ];
84
+ }
85
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: RangeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
86
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.7", type: RangeViewComponent, selector: "pdb-range-view", inputs: { domain: "domain", range: "range", width: "width", height: "height" }, usesOnChanges: true, 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"] }); }
87
+ }
88
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: RangeViewComponent, decorators: [{
89
+ type: Component,
90
+ 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"] }]
91
+ }], ctorParameters: function () { return []; }, propDecorators: { domain: [{
92
+ type: Input
93
+ }], range: [{
94
+ type: Input
95
+ }], width: [{
96
+ type: Input
97
+ }], height: [{
98
+ type: Input
99
+ }] } });
100
+
101
+ /**
102
+ * Browser tree node component view. This wraps some HTML content providing
103
+ * a toggle button to expand/collapse the node, a paging control for the
104
+ * node's children, and a button to edit the node's filter. You should then
105
+ * provide the HTML content to display the node's data inside this component, e.g.
106
+ * <pdb-browser-tree-node [node]="node">
107
+ * <your-node-view [node]="node" />
108
+ * <pdb-browser-tree-node>
109
+ */
110
+ class BrowserTreeNodeComponent {
111
+ /**
112
+ * The node to display.
113
+ */
114
+ get node() {
115
+ return this._node;
116
+ }
117
+ set node(value) {
118
+ if (this._node === value) {
119
+ return;
120
+ }
121
+ this._node = value;
122
+ }
123
+ constructor() {
124
+ this.toggleExpandedRequest = new EventEmitter();
125
+ this.changePageRequest = new EventEmitter();
126
+ this.editNodeFilterRequest = new EventEmitter();
127
+ }
128
+ onToggleExpanded() {
129
+ if (!this._node) {
130
+ return;
131
+ }
132
+ this.toggleExpandedRequest.emit(this._node);
133
+ }
134
+ onPagingChange(node, paging) {
135
+ this.changePageRequest.emit({
136
+ node,
137
+ paging,
138
+ });
139
+ }
140
+ onEditFilter() {
141
+ if (this._node) {
142
+ this.editNodeFilterRequest.emit(this._node);
143
+ }
144
+ }
145
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: BrowserTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
146
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.7", type: BrowserTreeNodeComponent, selector: "pdb-browser-tree-node", inputs: { node: "node", paging: "paging", debug: "debug", hidePaging: "hidePaging" }, outputs: { toggleExpandedRequest: "toggleExpandedRequest", changePageRequest: "changePageRequest", editNodeFilterRequest: "editNodeFilterRequest" }, ngImport: i0, template: "<div id=\"node\" *ngIf=\"node\" [style.margin-left.px]=\"(node.y - 1) * 20\">\r\n <!-- pager -->\r\n <div\r\n *ngIf=\"$any(node).expanded && paging && paging.pageCount > 1\"\r\n id=\"pager\"\r\n [style.display]=\"hidePaging ? 'inherit' : 'block'\"\r\n >\r\n <pdb-compact-pager\r\n [paging]=\"paging\"\r\n (pagingChange)=\"onPagingChange($any(node), $event)\"\r\n />\r\n <pdb-range-view\r\n [width]=\"250\"\r\n [domain]=\"[0, paging.pageCount]\"\r\n [range]=\"[paging.pageNumber - 1, paging.pageNumber]\"\r\n />\r\n </div>\r\n <!-- node -->\r\n <div class=\"form-row\">\r\n <!-- expand/collapse button -->\r\n <button\r\n *ngIf=\"node.y > 0\"\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n [matTooltip]=\"$any(node).expanded ? 'Collapse' : 'Expand'\"\r\n [disabled]=\"node.hasChildren === false\"\r\n (click)=\"onToggleExpanded()\"\r\n >\r\n <mat-icon>{{\r\n node.hasChildren === true || node.hasChildren === undefined\r\n ? $any(node).expanded\r\n ? \"expand_less\"\r\n : \"expand_more\"\r\n : \"stop\"\r\n }}</mat-icon>\r\n </button>\r\n <!-- tag -->\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 <!-- loc and label -->\r\n <span class=\"loc\">{{ node.y }}.{{ node.x }}</span> - {{ node.label }}\r\n\r\n <!-- PROJECTED NODE -->\r\n <ng-content></ng-content>\r\n\r\n <!-- debug -->\r\n <span *ngIf=\"debug\" class=\"debug\"\r\n >#{{ node.id }}\r\n <span\r\n >| {{ $any(node).paging.pageNumber }}/{{\r\n $any(node).paging.pageCount\r\n }}\r\n ({{ $any(node).paging.total }})</span\r\n ></span\r\n >\r\n\r\n <!-- filter -->\r\n <div class=\"muted\" *ngIf=\"!$any(node).filter && node.y\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Add filter\"\r\n (click)=\"onEditFilter()\"\r\n >\r\n <mat-icon>filter_list</mat-icon>\r\n </button>\r\n </div>\r\n <div class=\"muted\" *ngIf=\"$any(node).filter && node.y\">\r\n <button type=\"button\" mat-icon-button (click)=\"onEditFilter()\">\r\n <mat-icon [matBadge]=\"$any(node).filter ? 'F' : ''\"\r\n >filter_alt</mat-icon\r\n >\r\n </button>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".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}#node{margin-bottom:4px;padding:4px 6px;border:1px solid transparent;border-radius:6px}#node:hover{border:1px solid #98a8d4;border-radius:6px}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: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: i2$1.MatBadge, selector: "[matBadge]", inputs: ["matBadgeDisabled", "matBadgeColor", "matBadgeOverlap", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "component", type: i2.MatIconButton, selector: "button[mat-icon-button]", inputs: ["disabled", "disableRipple", "color"], exportAs: ["matButton"] }, { kind: "component", type: i3.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", exportAs: ["matTooltip"] }, { kind: "component", type: 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: i8.ColorToContrastPipe, name: "colorToContrast" }, { kind: "pipe", type: i8.StringToColorPipe, name: "stringToColor" }] }); }
147
+ }
148
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: BrowserTreeNodeComponent, decorators: [{
149
+ type: Component,
150
+ args: [{ selector: 'pdb-browser-tree-node', template: "<div id=\"node\" *ngIf=\"node\" [style.margin-left.px]=\"(node.y - 1) * 20\">\r\n <!-- pager -->\r\n <div\r\n *ngIf=\"$any(node).expanded && paging && paging.pageCount > 1\"\r\n id=\"pager\"\r\n [style.display]=\"hidePaging ? 'inherit' : 'block'\"\r\n >\r\n <pdb-compact-pager\r\n [paging]=\"paging\"\r\n (pagingChange)=\"onPagingChange($any(node), $event)\"\r\n />\r\n <pdb-range-view\r\n [width]=\"250\"\r\n [domain]=\"[0, paging.pageCount]\"\r\n [range]=\"[paging.pageNumber - 1, paging.pageNumber]\"\r\n />\r\n </div>\r\n <!-- node -->\r\n <div class=\"form-row\">\r\n <!-- expand/collapse button -->\r\n <button\r\n *ngIf=\"node.y > 0\"\r\n type=\"button\"\r\n mat-icon-button\r\n color=\"primary\"\r\n [matTooltip]=\"$any(node).expanded ? 'Collapse' : 'Expand'\"\r\n [disabled]=\"node.hasChildren === false\"\r\n (click)=\"onToggleExpanded()\"\r\n >\r\n <mat-icon>{{\r\n node.hasChildren === true || node.hasChildren === undefined\r\n ? $any(node).expanded\r\n ? \"expand_less\"\r\n : \"expand_more\"\r\n : \"stop\"\r\n }}</mat-icon>\r\n </button>\r\n <!-- tag -->\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 <!-- loc and label -->\r\n <span class=\"loc\">{{ node.y }}.{{ node.x }}</span> - {{ node.label }}\r\n\r\n <!-- PROJECTED NODE -->\r\n <ng-content></ng-content>\r\n\r\n <!-- debug -->\r\n <span *ngIf=\"debug\" class=\"debug\"\r\n >#{{ node.id }}\r\n <span\r\n >| {{ $any(node).paging.pageNumber }}/{{\r\n $any(node).paging.pageCount\r\n }}\r\n ({{ $any(node).paging.total }})</span\r\n ></span\r\n >\r\n\r\n <!-- filter -->\r\n <div class=\"muted\" *ngIf=\"!$any(node).filter && node.y\">\r\n <button\r\n type=\"button\"\r\n mat-icon-button\r\n matTooltip=\"Add filter\"\r\n (click)=\"onEditFilter()\"\r\n >\r\n <mat-icon>filter_list</mat-icon>\r\n </button>\r\n </div>\r\n <div class=\"muted\" *ngIf=\"$any(node).filter && node.y\">\r\n <button type=\"button\" mat-icon-button (click)=\"onEditFilter()\">\r\n <mat-icon [matBadge]=\"$any(node).filter ? 'F' : ''\"\r\n >filter_alt</mat-icon\r\n >\r\n </button>\r\n </div>\r\n </div>\r\n</div>\r\n", styles: [".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}#node{margin-bottom:4px;padding:4px 6px;border:1px solid transparent;border-radius:6px}#node:hover{border:1px solid #98a8d4;border-radius:6px}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"] }]
151
+ }], ctorParameters: function () { return []; }, propDecorators: { node: [{
152
+ type: Input
153
+ }], paging: [{
154
+ type: Input
155
+ }], debug: [{
156
+ type: Input
157
+ }], hidePaging: [{
158
+ type: Input
159
+ }], toggleExpandedRequest: [{
160
+ type: Output
161
+ }], changePageRequest: [{
162
+ type: Output
163
+ }], editNodeFilterRequest: [{
164
+ type: Output
165
+ }] } });
166
+
167
+ /**
168
+ * A Least Recently Used cache that can be used to store any type of object.
169
+ * The cache works in two modes: considering the size of the objects or not.
170
+ * If the size is considered, the cache will have a maximum size and will
171
+ * remove the oldest objects when the maximum size is reached. Note that
172
+ * the size is only roughly estimated. This avoids removing too many
173
+ * entries from the cache when the maximum is reached.
174
+ * If the size is not considered, the cache will have a maximum number of
175
+ * objects and will remove the oldest objects when the maximum number is
176
+ * reached.
177
+ */
178
+ class LRUCache {
179
+ /**
180
+ * Creates a new cache.
181
+ * @param maxSize The maximum size of the cache. This is either
182
+ * the maximum number of items in the cache (when considerSize
183
+ * is false) or the maximum total size of all items in the
184
+ * cache in bytes (when considerSize is true).
185
+ * @param considerSize True if the size of the objects should be
186
+ * considered.
187
+ */
188
+ constructor(maxSize, considerSize = false) {
189
+ this.maxSize = maxSize;
190
+ this.totalSize = 0;
191
+ this.considerSize = considerSize;
192
+ this.cache = new Map();
193
+ this.sizes = new Map();
194
+ }
195
+ /**
196
+ * Get an item from the cache.
197
+ * @param key The key of the item to get.
198
+ * @returns The item or undefined if the item is not in the cache.
199
+ */
200
+ get(key) {
201
+ let item = this.cache.get(key);
202
+ if (item) {
203
+ this.cache.delete(key);
204
+ this.cache.set(key, item);
205
+ }
206
+ return item;
207
+ }
208
+ /**
209
+ * Put an item in the cache.
210
+ * @param key The key of the item to put.
211
+ * @param item The item to put.
212
+ * @param size The estimated size of the item in bytes.
213
+ * This must be calculated by the caller but only when
214
+ * considerSize is true.
215
+ */
216
+ put(key, item, size) {
217
+ this.cache.delete(key);
218
+ this.cache.set(key, item);
219
+ this.sizes.set(key, size);
220
+ if (this.considerSize) {
221
+ this.totalSize += size;
222
+ while (this.totalSize > this.maxSize) {
223
+ const oldestKey = this.cache.keys().next().value;
224
+ let oldestSize = this.sizes.get(oldestKey);
225
+ if (oldestSize) {
226
+ this.totalSize -= oldestSize;
227
+ }
228
+ this.cache.delete(oldestKey);
229
+ this.sizes.delete(oldestKey);
230
+ }
231
+ }
232
+ else {
233
+ while (this.cache.size > this.maxSize) {
234
+ const oldestKey = this.cache.keys().next().value;
235
+ this.cache.delete(oldestKey);
236
+ this.sizes.delete(oldestKey);
237
+ }
238
+ }
239
+ }
240
+ /**
241
+ * Clear the cache.
242
+ */
243
+ clear() {
244
+ this.cache.clear();
245
+ this.sizes.clear();
246
+ this.totalSize = 0;
247
+ }
248
+ /**
249
+ * Estimate the size of an object in bytes.
250
+ * @param obj The object to calculate the size of.
251
+ * @returns The estimated size of the object in bytes.
252
+ */
253
+ static calculateObjectSize(obj) {
254
+ if (!obj) {
255
+ return 0;
256
+ }
257
+ let totalSize = 0;
258
+ let keys = Object.keys(obj);
259
+ for (let key of keys) {
260
+ let value = obj[key];
261
+ if (typeof value === 'string') {
262
+ totalSize += value.length * 2;
263
+ }
264
+ else if (typeof value === 'number') {
265
+ totalSize += 8;
266
+ }
267
+ else if (typeof value === 'boolean') {
268
+ totalSize += 4;
269
+ }
270
+ else if (typeof value === 'object' && value !== null) {
271
+ totalSize += this.calculateObjectSize(value);
272
+ }
273
+ }
274
+ return totalSize;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Default options for the paged list store.
280
+ */
281
+ const DEFAULT_PAGED_LIST_STORE_OPTIONS = {
282
+ pageSize: 20,
283
+ cacheSize: 50,
284
+ };
285
+ /**
286
+ * A generic paged list store using a filter object of type F
287
+ * and a list of elements of type E.
288
+ */
289
+ class PagedListStore {
290
+ /**
291
+ * The size of nodes pages in this store. If you change it, the store
292
+ * is reset. The default value is 20.
293
+ */
294
+ get pageSize() {
295
+ return this._pageSize;
296
+ }
297
+ set pageSize(value) {
298
+ if (this._pageSize === value) {
299
+ return;
300
+ }
301
+ this._pageSize = value;
302
+ this.reset();
303
+ }
304
+ /**
305
+ * Create a new paged list store.
306
+ * @param options Options for the paged list store.
307
+ */
308
+ constructor(_service, options = DEFAULT_PAGED_LIST_STORE_OPTIONS) {
309
+ this._service = _service;
310
+ this._pageSize = options.pageSize;
311
+ this._cache = new LRUCache(options.cacheSize);
312
+ // page
313
+ this._page$ = new BehaviorSubject({
314
+ pageNumber: 0,
315
+ pageCount: 0,
316
+ pageSize: 0,
317
+ total: 0,
318
+ items: [],
319
+ });
320
+ this.page$ = this._page$.asObservable();
321
+ // filter
322
+ this._filter$ = new BehaviorSubject({});
323
+ this.filter$ = this._filter$.asObservable();
324
+ }
325
+ /**
326
+ * Returns true if the store is empty, false otherwise.
327
+ * @returns true if the store is empty, false otherwise.
328
+ */
329
+ isEmpty() {
330
+ return this._page$.value.items.length === 0;
331
+ }
332
+ /**
333
+ * Build the cache key for the given page number and filter.
334
+ * The default implementation just returns a stringified object
335
+ * containing the page number and the filter. You may override
336
+ * this method to provide a custom cache key.
337
+ * @param pageNumber The page number.
338
+ * @param filter The filter.
339
+ * @returns A string to be used as cache key.
340
+ */
341
+ buildCacheKey(pageNumber, filter) {
342
+ if (this._customCacheKeyBuilder) {
343
+ return this._customCacheKeyBuilder(pageNumber, filter);
344
+ }
345
+ return JSON.stringify({ pageNumber, ...filter });
346
+ }
347
+ /**
348
+ * Load the page with the given number.
349
+ * @param pageNumber the page number to load.
350
+ */
351
+ loadPage(pageNumber) {
352
+ return this._service.loadPage(pageNumber, this._pageSize, this._filter$.value);
353
+ }
354
+ /**
355
+ * Set the page with the given number.
356
+ * @param pageNumber The page number to load.
357
+ * @param pageSize The page size.
358
+ * @returns Promise which resolves when the page is loaded.
359
+ */
360
+ setPage(pageNumber, pageSize) {
361
+ if (pageSize && pageSize !== this._pageSize) {
362
+ this._pageSize = pageSize;
363
+ }
364
+ return new Promise((resolve, reject) => {
365
+ // if page is in cache, return it
366
+ const key = this.buildCacheKey(pageNumber, this._filter$.value);
367
+ const cachedPage = this._cache.get(key);
368
+ if (cachedPage) {
369
+ this._page$.next(cachedPage);
370
+ resolve();
371
+ return;
372
+ }
373
+ // else load page
374
+ this.loadPage(pageNumber).subscribe({
375
+ next: (page) => {
376
+ this._page$.next(page);
377
+ resolve();
378
+ },
379
+ error: reject,
380
+ });
381
+ });
382
+ }
383
+ /**
384
+ * Apply the given filter and load the first page.
385
+ * @param filter The filter to apply.
386
+ * @returns Promise which resolves when the page is loaded.
387
+ */
388
+ applyFilter(filter) {
389
+ return new Promise((resolve, reject) => {
390
+ this._filter$.next(filter);
391
+ this.setPage(1).then(resolve, reject);
392
+ });
393
+ }
394
+ /**
395
+ * Reset the filter and load the first page. The cache is cleared.
396
+ * @returns Promise which resolves when the page is loaded.
397
+ */
398
+ reset() {
399
+ this._cache.clear();
400
+ return this.applyFilter({});
401
+ }
402
+ }
403
+
404
+ /**
405
+ * A store for the node browser component. This store is used to keep a
406
+ * list of nodes, and to load them from the API. It also keeps the root
407
+ * node. Every tree node in the list is extended with page number,
408
+ * page count and total items, plus expansion-related metadata.
409
+ * The store keeps a flat list of these tree nodes, allowing users to
410
+ * expand and collapse them.
411
+ * F is the type of the filter object, E is the type of the paged tree nodes.
412
+ */
413
+ class PagedTreeStore {
414
+ /**
415
+ * The size of nodes pages in this store. If you change it, the store
416
+ * is reset. The default value is 20.
417
+ */
418
+ get pageSize() {
419
+ return this._pageSize;
420
+ }
421
+ set pageSize(value) {
422
+ if (this._pageSize === value) {
423
+ return;
424
+ }
425
+ this._pageSize = value;
426
+ this.reset(this._radix?.label || this._radixLabel);
427
+ }
428
+ /**
429
+ * Create an instance of the store.
430
+ * @param _service The service used to load nodes.
431
+ * @param options The options to configure this store.
432
+ */
433
+ constructor(_service, options = DEFAULT_PAGED_LIST_STORE_OPTIONS) {
434
+ this._service = _service;
435
+ this._pageSize = options.pageSize;
436
+ this._cache = new LRUCache(options.cacheSize);
437
+ this._customCacheKeyBuilder = options.buildCacheKey;
438
+ this._radixLabel = '(root)';
439
+ this._roots = [];
440
+ this._nodes$ = new BehaviorSubject([]);
441
+ this.nodes$ = this._nodes$.asObservable();
442
+ this._tags$ = new BehaviorSubject([]);
443
+ this.tags$ = this._tags$.asObservable();
444
+ this._filter$ = new BehaviorSubject({});
445
+ this.filter$ = this._filter$.asObservable();
446
+ this._dirty = true;
447
+ this.updateTags();
448
+ }
449
+ updateTags() {
450
+ this._service.getTags().subscribe((tags) => {
451
+ this._tags$.next(tags);
452
+ });
453
+ }
454
+ /**
455
+ * Gets the global filter eventually overridden with values
456
+ * from the specified node's filter.
457
+ * @param node The optional node.
458
+ * @returns The filter.
459
+ */
460
+ getFilter(node) {
461
+ return node?.filter
462
+ ? {
463
+ ...this._filter$.value,
464
+ ...node.filter,
465
+ }
466
+ : this._filter$.value;
467
+ }
468
+ /**
469
+ * Gets all the nodes in the store.
470
+ * @returns The nodes.
471
+ */
472
+ getNodes() {
473
+ return this._nodes$.value;
474
+ }
475
+ /**
476
+ * Gets the list of nodes tags.
477
+ * @returns The tags.
478
+ */
479
+ getTags() {
480
+ return this._tags$.value;
481
+ }
482
+ /**
483
+ * Build the cache key for the given page number and filter.
484
+ * The default implementation just returns a stringified object
485
+ * containing the page number and the filter. You may override
486
+ * this method to provide a custom cache key.
487
+ * @param pageNumber The page number.
488
+ * @param filter The filter.
489
+ * @returns A string to be used as cache key.
490
+ */
491
+ buildCacheKey(pageNumber, filter) {
492
+ if (this._customCacheKeyBuilder) {
493
+ return this._customCacheKeyBuilder(pageNumber, filter);
494
+ }
495
+ return JSON.stringify({ pageNumber, ...filter });
496
+ }
497
+ getPageFromCacheOrServer(filter, pageNumber) {
498
+ const key = this.buildCacheKey(pageNumber, filter);
499
+ const pageInCache = this._cache.get(key);
500
+ if (pageInCache) {
501
+ return of(pageInCache);
502
+ }
503
+ else {
504
+ return this._service.getNodes(filter, pageNumber, this._pageSize).pipe(tap((page) => {
505
+ this._cache.put(key, page, 0);
506
+ }));
507
+ }
508
+ }
509
+ createPageNodes(page) {
510
+ return page.items.map((n) => {
511
+ return {
512
+ ...n,
513
+ hasChildren: n.hasChildren,
514
+ paging: {
515
+ pageNumber: page.pageNumber,
516
+ pageCount: page.pageCount,
517
+ total: page.total,
518
+ },
519
+ };
520
+ });
521
+ }
522
+ /**
523
+ * Applies the filter for this store. Whenever the filter is set,
524
+ * the store is reset.
525
+ * @param filter The filter.
526
+ * @param radixLabel The label of the radix node, if this needs to be set.
527
+ * @returns true if tree was changed, false otherwise.
528
+ */
529
+ applyFilter(filter, radixLabel) {
530
+ if (this._filter$.value === filter) {
531
+ return Promise.resolve(false);
532
+ }
533
+ this._filter$.next(filter);
534
+ this._dirty = true;
535
+ return this.reset(this._radix?.label || radixLabel || this._radixLabel);
536
+ }
537
+ /**
538
+ * Reset the store, loading the root nodes and their children.
539
+ * @param label The label of the radix node.
540
+ * @returns true if tree was changed, false otherwise.
541
+ */
542
+ reset(label) {
543
+ if (!this._dirty) {
544
+ return Promise.resolve(false);
545
+ }
546
+ this._cache.clear();
547
+ const filter = this._filter$.value;
548
+ this._radix = {
549
+ id: 0,
550
+ y: 0,
551
+ x: 1,
552
+ label: label,
553
+ paging: {
554
+ pageNumber: 0,
555
+ pageCount: 0,
556
+ total: 0,
557
+ },
558
+ };
559
+ return new Promise((resolve, reject) => {
560
+ this._service
561
+ .getRootNodes(filter.tags)
562
+ .pipe(switchMap((nodes) => {
563
+ // no roots, clear and return empty set
564
+ if (!nodes || nodes.length === 0) {
565
+ this._roots = [];
566
+ return of([]);
567
+ }
568
+ else {
569
+ // got roots, set them and get their children
570
+ this._roots = nodes.map((node) => ({
571
+ ...node,
572
+ paging: {
573
+ pageNumber: 1,
574
+ pageCount: 1,
575
+ total: 1,
576
+ },
577
+ }));
578
+ // fetch children for each root node
579
+ return forkJoin(this._roots.map((root) => this.getPageFromCacheOrServer({ ...filter, parentId: root.id }, 1)));
580
+ }
581
+ }))
582
+ .subscribe({
583
+ next: (pages) => {
584
+ this._dirty = false;
585
+ if (pages.some((page) => page.total)) {
586
+ // radix
587
+ this._radix.hasChildren = true;
588
+ this._radix.expanded = true;
589
+ this._radix.paging = {
590
+ pageNumber: 1,
591
+ pageCount: 1,
592
+ total: pages.length,
593
+ };
594
+ // roots
595
+ this._roots.forEach((root, i) => {
596
+ root.hasChildren = !!pages[i].total;
597
+ root.expanded = !!pages[i].total;
598
+ });
599
+ const nodes = this._roots.flatMap((root, i) => [
600
+ root,
601
+ ...this.createPageNodes(pages[i]),
602
+ ]);
603
+ this._nodes$.next([this._radix, ...nodes]);
604
+ resolve(true);
605
+ }
606
+ else {
607
+ this._roots.forEach((root) => {
608
+ root.hasChildren = false;
609
+ root.expanded = false;
610
+ });
611
+ this._nodes$.next([this._radix, ...this._roots]);
612
+ resolve(true);
613
+ }
614
+ },
615
+ error: (error) => {
616
+ reject(error);
617
+ },
618
+ });
619
+ });
620
+ }
621
+ /**
622
+ * Set the node filter for the node with the specified ID.
623
+ * @param id The node ID.
624
+ * @param filter The filter to set.
625
+ * @returns Promise with true if filter was set, false otherwise.
626
+ */
627
+ setNodeFilter(id, filter) {
628
+ if (!id) {
629
+ return Promise.resolve(false);
630
+ }
631
+ return new Promise((resolve, reject) => {
632
+ const node = this._nodes$.value.find((n) => n.id === id);
633
+ if (!node) {
634
+ reject(`Node ID ${id} not found in store`);
635
+ }
636
+ node.filter = filter || undefined;
637
+ return this.changePage(id, 1);
638
+ });
639
+ }
640
+ /**
641
+ * Expand the node with the specified ID. If the node is not expandable,
642
+ * or it is already expanded, this method does nothing.
643
+ * @param node The ID of the node to expand.
644
+ * @returns Promise with true if the node was expanded, false otherwise.
645
+ */
646
+ expand(id) {
647
+ if (!id) {
648
+ return Promise.resolve(false);
649
+ }
650
+ return new Promise((resolve, reject) => {
651
+ const node = this._nodes$.value.find((n) => n.id === id);
652
+ if (!node || node.hasChildren === false || node.expanded) {
653
+ resolve(false);
654
+ }
655
+ this.getPageFromCacheOrServer({ ...this.getFilter(node), parentId: id }, 1).subscribe((page) => {
656
+ // no children, set hasChildren to false
657
+ if (!page.total) {
658
+ node.hasChildren = false;
659
+ resolve(false);
660
+ }
661
+ else {
662
+ this._dirty = true;
663
+ // insert page nodes after the current node
664
+ const nodes = this._nodes$.value;
665
+ const index = nodes.indexOf(node);
666
+ if (index === -1) {
667
+ reject(`Node ID ${id} not found in store`);
668
+ }
669
+ else {
670
+ const pageNodes = this.createPageNodes(page);
671
+ nodes.splice(index + 1, 0, ...pageNodes);
672
+ this._nodes$.next(nodes);
673
+ node.hasChildren = true;
674
+ node.expanded = true;
675
+ resolve(true);
676
+ }
677
+ }
678
+ });
679
+ });
680
+ }
681
+ expandAll(id) {
682
+ if (!id) {
683
+ return Promise.resolve(false);
684
+ }
685
+ // get the parent node to start from
686
+ const nodes = this._nodes$.value;
687
+ const nodeIndex = nodes.findIndex((n) => n.id === id);
688
+ if (nodeIndex === -1) {
689
+ return Promise.resolve(false);
690
+ }
691
+ // collect all the descendant nodes IDs
692
+ let i = nodeIndex + 1;
693
+ while (i < nodes.length && nodes[i].y > nodes[nodeIndex].y) {
694
+ i++;
695
+ }
696
+ const nodesToExpand = nodes.slice(nodeIndex, i).map((n) => n.id);
697
+ // expand all the descendant nodes
698
+ return new Promise((resolve, reject) => {
699
+ nodesToExpand.forEach((id) => {
700
+ this.expand(id);
701
+ this.expandAll(id);
702
+ });
703
+ resolve(true);
704
+ });
705
+ }
706
+ getChildren(id) {
707
+ const node = this._nodes$.value.find((n) => n.id === id);
708
+ if (!node || node.hasChildren === false) {
709
+ return [];
710
+ }
711
+ const nodes = this._nodes$.value;
712
+ const index = nodes.indexOf(node);
713
+ if (index === -1) {
714
+ return [];
715
+ }
716
+ const children = [];
717
+ let i = index + 1;
718
+ while (i < nodes.length && nodes[i].y > node.y) {
719
+ children.push(nodes[i]);
720
+ i++;
721
+ }
722
+ return children;
723
+ }
724
+ removeDescendants(nodes, nodeIndex) {
725
+ let i = nodeIndex + 1;
726
+ while (i < nodes.length && nodes[i].y > nodes[nodeIndex].y) {
727
+ i++;
728
+ }
729
+ nodes.splice(nodeIndex + 1, i - nodeIndex - 1);
730
+ }
731
+ /**
732
+ * Collapse the node with the specified ID. If the node is not expandable,
733
+ * or it is already collapsed, this method does nothing.
734
+ * @param node The node to collapse.
735
+ * @returns Promise with true if the node was collapsed, false otherwise.
736
+ */
737
+ collapse(id) {
738
+ if (!id) {
739
+ return Promise.resolve(false);
740
+ }
741
+ return new Promise((resolve, reject) => {
742
+ const node = this._nodes$.value.find((n) => n.id === id);
743
+ if (!node || node.hasChildren === false || !node.expanded) {
744
+ resolve(false);
745
+ }
746
+ // remove all the descendant nodes after the current node
747
+ const nodes = this._nodes$.value;
748
+ const nodeIndex = nodes.indexOf(node);
749
+ if (nodeIndex === -1) {
750
+ reject(`Node ID ${id} not found in store`);
751
+ }
752
+ else {
753
+ this._dirty = true;
754
+ this.removeDescendants(nodes, nodeIndex);
755
+ this._nodes$.next(nodes);
756
+ node.expanded = false;
757
+ resolve(true);
758
+ }
759
+ });
760
+ }
761
+ /**
762
+ * Change the page including the node with the specified ID.
763
+ * @param node The parent node whose children are inside the page you want to change.
764
+ * @param pageNumber The new page number.
765
+ * @returns Promise with true if the page was changed, false otherwise.
766
+ */
767
+ changePage(parentId, pageNumber) {
768
+ return new Promise((resolve, reject) => {
769
+ const parentNode = this._nodes$.value.find((n) => n.id === parentId);
770
+ if (!parentNode) {
771
+ resolve(false);
772
+ }
773
+ this.getPageFromCacheOrServer({ ...this.getFilter(parentNode), parentId }, pageNumber).subscribe((page) => {
774
+ // if page is empty do nothing
775
+ if (!page.total) {
776
+ resolve(false);
777
+ }
778
+ else {
779
+ this._dirty = true;
780
+ // remove all the nodes in the same page of node
781
+ // with all their descendants
782
+ const nodes = this._nodes$.value;
783
+ const nodeIndex = nodes.indexOf(parentNode) + 1;
784
+ const pageNodes = this.createPageNodes(page);
785
+ // find the first node of the node's page
786
+ let start = nodeIndex;
787
+ const oldPageNr = nodes[start].paging.pageNumber;
788
+ while (start > 0 &&
789
+ nodes[start - 1].parentId === parentId &&
790
+ nodes[start - 1].paging.pageNumber === oldPageNr) {
791
+ start--;
792
+ }
793
+ // find the last node of the node's page,
794
+ // including all their descendants
795
+ let end = nodeIndex + 1;
796
+ while (end < nodes.length &&
797
+ nodes[end].parentId === parentId &&
798
+ nodes[end].paging.pageNumber === oldPageNr) {
799
+ end++;
800
+ }
801
+ // replace all these nodes with the new ones
802
+ nodes.splice(start, end - start);
803
+ nodes.splice(start, 0, ...pageNodes);
804
+ // update the parent node paging info
805
+ parentNode.paging.pageNumber = page.pageNumber;
806
+ this._nodes$.next(nodes);
807
+ resolve(true);
808
+ }
809
+ });
810
+ });
811
+ }
812
+ /**
813
+ * Collapse all the nodes in the store.
814
+ */
815
+ collapseAll() {
816
+ this._nodes$.next([this._radix, ...this._roots]);
817
+ }
818
+ }
819
+
820
+ class PagedDataBrowsersModule {
821
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: PagedDataBrowsersModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
822
+ static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.2.7", ngImport: i0, type: PagedDataBrowsersModule, declarations: [CompactPagerComponent,
823
+ RangeViewComponent,
824
+ BrowserTreeNodeComponent], imports: [CommonModule,
825
+ ReactiveFormsModule,
826
+ // material
827
+ MatBadgeModule,
828
+ MatButtonModule,
829
+ MatChipsModule,
830
+ MatDialogModule,
831
+ MatFormFieldModule,
832
+ MatIconModule,
833
+ MatInputModule,
834
+ MatPaginatorModule,
835
+ MatProgressBarModule,
836
+ MatSelectModule,
837
+ MatTooltipModule,
838
+ // myrmidon
839
+ NgToolsModule], exports: [CompactPagerComponent,
840
+ RangeViewComponent,
841
+ BrowserTreeNodeComponent] }); }
842
+ static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: PagedDataBrowsersModule, imports: [CommonModule,
843
+ ReactiveFormsModule,
844
+ // material
845
+ MatBadgeModule,
846
+ MatButtonModule,
847
+ MatChipsModule,
848
+ MatDialogModule,
849
+ MatFormFieldModule,
850
+ MatIconModule,
851
+ MatInputModule,
852
+ MatPaginatorModule,
853
+ MatProgressBarModule,
854
+ MatSelectModule,
855
+ MatTooltipModule,
856
+ // myrmidon
857
+ NgToolsModule] }); }
858
+ }
859
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.7", ngImport: i0, type: PagedDataBrowsersModule, decorators: [{
860
+ type: NgModule,
861
+ args: [{
862
+ declarations: [
863
+ CompactPagerComponent,
864
+ RangeViewComponent,
865
+ BrowserTreeNodeComponent,
866
+ ],
867
+ imports: [
868
+ CommonModule,
869
+ ReactiveFormsModule,
870
+ // material
871
+ MatBadgeModule,
872
+ MatButtonModule,
873
+ MatChipsModule,
874
+ MatDialogModule,
875
+ MatFormFieldModule,
876
+ MatIconModule,
877
+ MatInputModule,
878
+ MatPaginatorModule,
879
+ MatProgressBarModule,
880
+ MatSelectModule,
881
+ MatTooltipModule,
882
+ // myrmidon
883
+ NgToolsModule
884
+ ],
885
+ exports: [
886
+ CompactPagerComponent,
887
+ RangeViewComponent,
888
+ BrowserTreeNodeComponent,
889
+ ],
890
+ }]
891
+ }] });
892
+
893
+ /*
894
+ * Public API Surface of paged-data-browsers
895
+ */
896
+
897
+ /**
898
+ * Generated bundle index. Do not edit.
899
+ */
900
+
901
+ export { BrowserTreeNodeComponent, CompactPagerComponent, DEFAULT_PAGED_LIST_STORE_OPTIONS, LRUCache, PagedDataBrowsersModule, PagedListStore, PagedTreeStore, RangeViewComponent };
902
+ //# sourceMappingURL=myrmidon-paged-data-browsers.mjs.map