@scion/components 22.6.0 → 22.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/design/components/_scion-component-dark-tokens.scss +15 -0
- package/design/components/_scion-component-light-tokens.scss +15 -0
- package/fesm2022/scion-components-common.mjs +25 -2
- package/fesm2022/scion-components-common.mjs.map +1 -1
- package/fesm2022/scion-components-table.mjs +1202 -859
- package/fesm2022/scion-components-table.mjs.map +1 -1
- package/fesm2022/scion-components-tree.mjs +306 -0
- package/fesm2022/scion-components-tree.mjs.map +1 -0
- package/package.json +6 -2
- package/tree/package.json +4 -0
- package/types/scion-components-common.d.ts +22 -2
- package/types/scion-components-table.d.ts +686 -255
- package/types/scion-components-tree.d.ts +559 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { signal, inject, Injector, computed, DestroyRef, untracked, runInInjectionContext, onIdle, Injectable, DEFAULT_CURRENCY_CODE, InjectionToken, linkedSignal, isSignal, NgZone, effect, resource, assertNotInReactiveContext, assertInInjectionContext, Pipe, input, Component, output, viewChild, Directive, afterNextRender, ElementRef, debounced, viewChildren, ViewEncapsulation } from '@angular/core';
|
|
3
|
+
import { toObservable, rxResource } from '@angular/core/rxjs-interop';
|
|
4
|
+
import { LatestTaskExecutor } from '@scion/toolkit/common';
|
|
5
|
+
import { Arrays, Observables, Objects, clamp } from '@scion/toolkit/util';
|
|
3
6
|
import * as i1 from '@scion/components/common';
|
|
4
|
-
import {
|
|
7
|
+
import { ɵSingleActiveInjectorFactory as _SingleActiveInjectorFactory, coerceSignal, SCI_LOCALE, SCI_COMPONENTS_STORAGE, ɵcreateDestroyableInjector as _createDestroyableInjector, toLazyObservable, SciComponentOutletDirective, SciTooltipOnOverflowDirective, TOOLTIP_ON_OVERFLOW_DIRECTIVE_ADDITIONAL_TEXT, TOOLTIP_ON_OVERFLOW_DIRECTIVE_SOURCE_ELEMENT, SciAttributesDirective } from '@scion/components/common';
|
|
8
|
+
import { mapArray, subscribeIn } from '@scion/toolkit/operators';
|
|
5
9
|
import { formatDate, DATE_PIPE_DEFAULT_OPTIONS, NgTemplateOutlet } from '@angular/common';
|
|
6
10
|
import { text, SciTextPipe } from '@scion/components/text';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { LatestTaskExecutor } from '@scion/toolkit/common';
|
|
10
|
-
import { fromEvent, switchMap, concat, of, timer, firstValueFrom, defaultIfEmpty, throwError, combineLatestWith, tap, skip } from 'rxjs';
|
|
11
|
-
import { map, startWith, first } from 'rxjs/operators';
|
|
12
|
-
import { subscribeIn } from '@scion/toolkit/operators';
|
|
11
|
+
import { catchError, fromEvent, switchMap, concat, of, timer, combineLatestWith, finalize, firstValueFrom, skip } from 'rxjs';
|
|
12
|
+
import { map, startWith } from 'rxjs/operators';
|
|
13
13
|
import { SciScrollbarComponent } from '@scion/components/viewport';
|
|
14
14
|
import { dimension } from '@scion/components/dimension';
|
|
15
15
|
import { SciIconComponent } from '@scion/components/icon';
|
|
@@ -20,6 +20,309 @@ import { SciThrobberComponent } from '@scion/components/throbber';
|
|
|
20
20
|
import * as i2 from '@angular/forms';
|
|
21
21
|
import { FormsModule } from '@angular/forms';
|
|
22
22
|
|
|
23
|
+
/** @inheritDoc */
|
|
24
|
+
class ɵSciTableCellValuePreloader {
|
|
25
|
+
_executor = new LatestTaskExecutor();
|
|
26
|
+
_queue = new Array(); // PERF: Do not change to signal to avoid array re-allocation on enqueue (expensive); queue size is tracked via a separate signal.
|
|
27
|
+
_queueSize = signal(0, /* @ts-ignore */
|
|
28
|
+
...(ngDevMode ? [{ debugName: "_queueSize" }] : /* istanbul ignore next */ []));
|
|
29
|
+
_injector = inject(Injector);
|
|
30
|
+
/** @inheritDoc */
|
|
31
|
+
loading = computed(() => this._queueSize() > 0, /* @ts-ignore */
|
|
32
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
33
|
+
constructor() {
|
|
34
|
+
inject(DestroyRef).onDestroy(() => {
|
|
35
|
+
this._executor.destroy();
|
|
36
|
+
this.clear();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** @inheritDoc */
|
|
40
|
+
queue(cellValueFn) {
|
|
41
|
+
this._queue.push(cellValueFn);
|
|
42
|
+
this._queueSize.update(size => size + 1);
|
|
43
|
+
this._executor.submit(() => this.whenIdle().then(() => this.preloadQueuedBatch(PRELOAD_BATCH_SIZE)));
|
|
44
|
+
}
|
|
45
|
+
/** @inheritDoc */
|
|
46
|
+
clear() {
|
|
47
|
+
this._queue.length = 0;
|
|
48
|
+
untracked(() => this._queueSize.set(0));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Preloads the next batch of queued cell values, FIFO, then, if the queue is not empty, schedules the next batch.
|
|
52
|
+
*/
|
|
53
|
+
async preloadQueuedBatch(batchSize) {
|
|
54
|
+
this._queue.splice(0, batchSize).forEach(fn => fn());
|
|
55
|
+
this._queueSize.set(this._queue.length);
|
|
56
|
+
// Preload next batch when idle again.
|
|
57
|
+
if (this._queue.length) {
|
|
58
|
+
this._executor.submit(() => this.whenIdle().then(() => this.preloadQueuedBatch(PRELOAD_BATCH_SIZE)));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolves when the browser becomes idle.
|
|
63
|
+
*
|
|
64
|
+
* @see onIdle
|
|
65
|
+
*/
|
|
66
|
+
whenIdle() {
|
|
67
|
+
return runInInjectionContext(this._injector, onIdle);
|
|
68
|
+
}
|
|
69
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ɵSciTableCellValuePreloader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
70
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ɵSciTableCellValuePreloader });
|
|
71
|
+
}
|
|
72
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ɵSciTableCellValuePreloader, decorators: [{
|
|
73
|
+
type: Injectable
|
|
74
|
+
}], ctorParameters: () => [] });
|
|
75
|
+
const PRELOAD_BATCH_SIZE = 50_000;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Preloads cell values in the background when the browser is idle. This prevents lag with large datasets, in particular
|
|
79
|
+
* for expensive computations like formatting dates and generating ISO 8601 strings, providing faster sort and filter performance.
|
|
80
|
+
*
|
|
81
|
+
* If a cell's value is requested before background preloading finishes, it is computed synchronously.
|
|
82
|
+
*/
|
|
83
|
+
class SciTableCellValuePreloader {
|
|
84
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableCellValuePreloader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
85
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableCellValuePreloader, providedIn: 'root', useClass: ɵSciTableCellValuePreloader });
|
|
86
|
+
}
|
|
87
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableCellValuePreloader, decorators: [{
|
|
88
|
+
type: Injectable,
|
|
89
|
+
args: [{ providedIn: 'root', useClass: ɵSciTableCellValuePreloader }]
|
|
90
|
+
}] });
|
|
91
|
+
|
|
92
|
+
/*
|
|
93
|
+
* Copyright (c) 2018-2026 Swiss Federal Railways
|
|
94
|
+
*
|
|
95
|
+
* This program and the accompanying materials are made
|
|
96
|
+
* available under the terms of the Eclipse Public License 2.0
|
|
97
|
+
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
98
|
+
*
|
|
99
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
100
|
+
*/
|
|
101
|
+
class SciInMemoryTableDatasource {
|
|
102
|
+
_rows;
|
|
103
|
+
constructor(data, table, options) {
|
|
104
|
+
const tableCellValuePreloader = inject(SciTableCellValuePreloader);
|
|
105
|
+
const injectorFactory = new _SingleActiveInjectorFactory();
|
|
106
|
+
this._rows = computed(() => {
|
|
107
|
+
// Create new value function injector, releasing resources allocated in column value functions.
|
|
108
|
+
const injector = injectorFactory.create();
|
|
109
|
+
// Cancel preloading of previous data.
|
|
110
|
+
tableCellValuePreloader.clear();
|
|
111
|
+
return data()
|
|
112
|
+
.flatMap(item => createRow(item))
|
|
113
|
+
.reduce((map, row) => map.set(row.id, row), new Map());
|
|
114
|
+
function createRow(item, parent) {
|
|
115
|
+
const id = table.trackBy(item);
|
|
116
|
+
return [
|
|
117
|
+
{
|
|
118
|
+
id,
|
|
119
|
+
parentId: parent?.id,
|
|
120
|
+
item,
|
|
121
|
+
cells: table.columns().reduce((cells, column) => untracked(() => {
|
|
122
|
+
// PERF: Cell values are computed lazily (on idle or first use) to defer expensive computations like date formatting.
|
|
123
|
+
const tableCellValue = 'value' in column ? new eSciTableCellValue(tableCellValuePreloader, () => runInInjectionContext(injector, () => column.value(item)())) : undefined;
|
|
124
|
+
return cells.set(column.name, {
|
|
125
|
+
column: column,
|
|
126
|
+
value: () => tableCellValue?.value(),
|
|
127
|
+
});
|
|
128
|
+
}), new Map()),
|
|
129
|
+
},
|
|
130
|
+
// Child rows if a tree structure.
|
|
131
|
+
...options?.children?.(item).flatMap(child => createRow(child, { id })) ?? [],
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
}, /* @ts-ignore */
|
|
135
|
+
...(ngDevMode ? [{ debugName: "_rows" }] : /* istanbul ignore next */ []));
|
|
136
|
+
}
|
|
137
|
+
getTreeTableNodes(query) {
|
|
138
|
+
return toObservable(computed(() => {
|
|
139
|
+
const rows = this._rows();
|
|
140
|
+
// PERF: Do not track signals inside `Array.sort` to avoid Angular signal tracking overhead as the comparator runs repeatedly, degrading performance otherwise.
|
|
141
|
+
return untracked(() => [...rows.values()]
|
|
142
|
+
.filter(row => matchesRow(row, query.columnFilters, { coerceEnumValueToArray: true }) && matchesGlobalFilter(row, query.tableFilter, { coerceEnumValueToArray: false }))
|
|
143
|
+
.sort((a, b) => compareRows(a, b, query.sortCriteria))
|
|
144
|
+
.map(row => ({ id: row.id, parentId: row.parentId })));
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
getItems(ids) {
|
|
148
|
+
return toObservable(computed(() => ids.flatMap(id => this._rows().has(id) ? this._rows().get(id).item : [])));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Tests whether a row matches the given global filter.
|
|
153
|
+
*/
|
|
154
|
+
function matchesGlobalFilter(row, filter, options) {
|
|
155
|
+
if (!filter?.trim()) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
for (const cell of row.cells.values()) {
|
|
159
|
+
const text = cell.column.type === 'number' ? toNumber(filter) : filter;
|
|
160
|
+
if (text === undefined) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (matchesRow(row, [{ text, columnName: cell.column.name }], options)) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Tests whether a row matches the given column filters.
|
|
171
|
+
*/
|
|
172
|
+
function matchesRow(row, columnFilters, options) {
|
|
173
|
+
if (!columnFilters.length) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
for (const columnFilter of columnFilters) {
|
|
177
|
+
const cell = row.cells.get(columnFilter.columnName);
|
|
178
|
+
if ('value' in cell.column && cell.value() === undefined) { // Component and template columns have no value.
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const filter = cell.column.type === 'enum' && options.coerceEnumValueToArray ? Arrays.coerce(columnFilter.text) : columnFilter.text;
|
|
182
|
+
if (!cell.column.matches(filter, { item: row.item, value: cell.value() })) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Compares two rows based on the given sort criteria.
|
|
190
|
+
*/
|
|
191
|
+
function compareRows(row1, row2, sortCriteria) {
|
|
192
|
+
if (!sortCriteria.length) {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
for (const criterion of sortCriteria) {
|
|
196
|
+
const cell1 = row1.cells.get(criterion.columnName);
|
|
197
|
+
const cell2 = row2.cells.get(criterion.columnName);
|
|
198
|
+
const value1 = cell1.value();
|
|
199
|
+
const value2 = cell2.value();
|
|
200
|
+
// Always place `undefined` values at the end, independent of the sort direction. Note that component and template columns have no value.
|
|
201
|
+
if (value1 === undefined && value2 !== undefined) {
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
if (value2 === undefined && value1 !== undefined) {
|
|
205
|
+
return -1;
|
|
206
|
+
}
|
|
207
|
+
// Prevent calling comparator for value columns where both values are undefined. Note that component and template columns have no value.
|
|
208
|
+
if (value1 === undefined && value2 === undefined && 'value' in cell1.column) {
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
const comparison = cell1.column.compare({ item: row1.item, value: value1 }, { item: row2.item, value: value2 });
|
|
212
|
+
if (comparison !== 0) {
|
|
213
|
+
return criterion.direction === 'asc' ? comparison : -comparison;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Converts the given string to a number, or `undefined` if it cannot be parsed.
|
|
220
|
+
*/
|
|
221
|
+
function toNumber(numberLike) {
|
|
222
|
+
const number = Number.parseFloat(numberLike);
|
|
223
|
+
return !Number.isNaN(number) ? number : undefined;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Provides lazy loading for the cell's value, preloading it when idle for better sort/filter performance.
|
|
227
|
+
*
|
|
228
|
+
* The value is computed at most once, whether it gets preloaded during idle time or requested synchronously via {@link value()}.
|
|
229
|
+
*/
|
|
230
|
+
class eSciTableCellValue {
|
|
231
|
+
_valueFn;
|
|
232
|
+
_value;
|
|
233
|
+
constructor(preloader, _valueFn) {
|
|
234
|
+
this._valueFn = _valueFn;
|
|
235
|
+
preloader.queue(() => {
|
|
236
|
+
this._value ??= this._valueFn();
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
value() {
|
|
240
|
+
return this._value ??= untracked(() => this._valueFn());
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/*
|
|
245
|
+
* Copyright (c) 2018-2026 Swiss Federal Railways
|
|
246
|
+
*
|
|
247
|
+
* This program and the accompanying materials are made
|
|
248
|
+
* available under the terms of the Eclipse Public License 2.0
|
|
249
|
+
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
250
|
+
*
|
|
251
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
252
|
+
*/
|
|
253
|
+
/**
|
|
254
|
+
* Provides an in-memory datasource for {@link SciTable} with built-in filtering and sorting.
|
|
255
|
+
*
|
|
256
|
+
* A datasource provides data to {@link SciTable}, passing each item to columns for display. For a tree structure, additionally pass a child selector to the datasource.
|
|
257
|
+
*
|
|
258
|
+
* This datasource requires all data to be loaded upfront. The datasource filters and sorts data in memory.
|
|
259
|
+
*
|
|
260
|
+
* Custom columns must configure a filter matcher and sort comparator to be filterable and sortable.
|
|
261
|
+
*
|
|
262
|
+
* For large datasets requiring pagination or server-side filtering and sorting, prefer implementing a custom datasource using {@link ɵprovideTableDatasource}.
|
|
263
|
+
*
|
|
264
|
+
* @example - Usage
|
|
265
|
+
* ```ts
|
|
266
|
+
* import {table, provideTableDatasource} from '@scion/components/table';
|
|
267
|
+
* import {httpResource} from '@angular/common/http';
|
|
268
|
+
*
|
|
269
|
+
* const users = httpResource<User[]>(() => '/api/users', {defaultValue: []});
|
|
270
|
+
*
|
|
271
|
+
* table({
|
|
272
|
+
* datasource: provideTableDatasource(users.value),
|
|
273
|
+
* columns: table => table.addStringColumn('Name', user => user.name),
|
|
274
|
+
* });
|
|
275
|
+
* ```
|
|
276
|
+
*
|
|
277
|
+
* @example - Displaying hierarchical data
|
|
278
|
+
* ```ts
|
|
279
|
+
* import {table, provideTableDatasource} from '@scion/components/table';
|
|
280
|
+
* import {httpResource} from '@angular/common/http';
|
|
281
|
+
*
|
|
282
|
+
* const users = httpResource<User[]>(() => '/api/users', {defaultValue: []});
|
|
283
|
+
*
|
|
284
|
+
* table({
|
|
285
|
+
* datasource: provideTableDatasource(users.value, user => user.children),
|
|
286
|
+
* columns: table => table.addStringColumn('Name', user => user.name),
|
|
287
|
+
* });
|
|
288
|
+
* ```
|
|
289
|
+
*
|
|
290
|
+
* @param data - Provides items to display in the table, or root items if displaying a tree structure.
|
|
291
|
+
* @param children - Function required to display a tree structure, returning an item's children or an empty array if the item has no children or is a leaf.
|
|
292
|
+
* Called eagerly for each item when initializing the table, inside a reactive context that re-evaluates when tracked signals change.
|
|
293
|
+
*
|
|
294
|
+
* @see SciTableDescriptor.datasource
|
|
295
|
+
*/
|
|
296
|
+
function provideTableDatasource(data, children) {
|
|
297
|
+
return {
|
|
298
|
+
[eSCI_TABLE_DATASOURCE_PROVIDER]: (table) => new SciInMemoryTableDatasource(data, table, { children }),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function ɵprovideTableDatasource(datasource) {
|
|
302
|
+
const isTreeTableDatesource = 'getTreeTableNodes' in datasource;
|
|
303
|
+
return {
|
|
304
|
+
[eSCI_TABLE_DATASOURCE_PROVIDER]: () => ({
|
|
305
|
+
getTreeTableNodes: (query) => isTreeTableDatesource ? datasource.getTreeTableNodes(query) : Observables.coerce(datasource.getIds(query)).pipe(mapArray(id => ({ id }))),
|
|
306
|
+
getItems: (ids) => datasource.getItems(ids),
|
|
307
|
+
}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Symbol for providing a {@link SciTreeTableDatasource}.
|
|
312
|
+
*/
|
|
313
|
+
const eSCI_TABLE_DATASOURCE_PROVIDER = Symbol('eSCI_TABLE_DATASOURCE_PROVIDER');
|
|
314
|
+
|
|
315
|
+
/*
|
|
316
|
+
* Copyright (c) 2018-2026 Swiss Federal Railways
|
|
317
|
+
*
|
|
318
|
+
* This program and the accompanying materials are made
|
|
319
|
+
* available under the terms of the Eclipse Public License 2.0
|
|
320
|
+
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
321
|
+
*
|
|
322
|
+
* SPDX-License-Identifier: EPL-2.0
|
|
323
|
+
*/
|
|
324
|
+
const skeletonRowId = Symbol('SKELETON_ROW');
|
|
325
|
+
|
|
23
326
|
/**
|
|
24
327
|
* Creates a {@link SciDateValue} from the given date-like input.
|
|
25
328
|
*
|
|
@@ -239,12 +542,12 @@ class ɵSciTableColumnFactory {
|
|
|
239
542
|
return this;
|
|
240
543
|
}
|
|
241
544
|
addComponentColumn(descriptor) {
|
|
242
|
-
// Require a filter matcher if filterable
|
|
243
|
-
if (this._table.
|
|
545
|
+
// Require a filter matcher if filterable using the built-in in-memory datasource.
|
|
546
|
+
if (this._table.isInMemoryDatasource() && descriptor.filterable === true) {
|
|
244
547
|
throw Error('[ColumnDefinitionError] Component column requires a filter matcher in order to be filterable.');
|
|
245
548
|
}
|
|
246
|
-
// Require a sort comparator if sortable
|
|
247
|
-
if (this._table.
|
|
549
|
+
// Require a sort comparator if sortable using the built-in in-memory datasource.
|
|
550
|
+
if (this._table.isInMemoryDatasource() && descriptor.sortable === true) {
|
|
248
551
|
throw Error('[ColumnDefinitionError] Component column requires a sort comparator in order to be sortable.');
|
|
249
552
|
}
|
|
250
553
|
const column = this.mapToColumn(descriptor);
|
|
@@ -261,12 +564,12 @@ class ɵSciTableColumnFactory {
|
|
|
261
564
|
return this;
|
|
262
565
|
}
|
|
263
566
|
addTemplateColumn(descriptor) {
|
|
264
|
-
// Require a filter matcher if filterable
|
|
265
|
-
if (this._table.
|
|
567
|
+
// Require a filter matcher if filterable using the built-in in-memory datasource.
|
|
568
|
+
if (this._table.isInMemoryDatasource() && descriptor.filterable === true) {
|
|
266
569
|
throw Error('[ColumnDefinitionError] Template column requires a filter matcher in order to be filterable.');
|
|
267
570
|
}
|
|
268
|
-
// Require a sort comparator if sortable
|
|
269
|
-
if (this._table.
|
|
571
|
+
// Require a sort comparator if sortable using the built-in in-memory datasource.
|
|
572
|
+
if (this._table.isInMemoryDatasource() && descriptor.sortable === true) {
|
|
270
573
|
throw Error('[ColumnDefinitionError] Template column requires a sort comparator in order to be sortable.');
|
|
271
574
|
}
|
|
272
575
|
const column = this.mapToColumn(descriptor);
|
|
@@ -291,12 +594,12 @@ class ɵSciTableColumnFactory {
|
|
|
291
594
|
if (this.columns.find(column => column.name === name)) {
|
|
292
595
|
throw Error(`[ColumnDefinitionError] Column names must be unique. "${name}" is defined more than once.`);
|
|
293
596
|
}
|
|
294
|
-
//
|
|
295
|
-
if (!this._table.
|
|
597
|
+
// Ensure filter matcher is only configured if using the built-in in-memory datasource.
|
|
598
|
+
if (!this._table.isInMemoryDatasource() && typeof descriptor.filterable === 'object') {
|
|
296
599
|
throw Error('[ColumnDefinitionError] Configuring a filter matcher is not supported for tables using a datasource. Filtering must be done by the datasource.');
|
|
297
600
|
}
|
|
298
|
-
//
|
|
299
|
-
if (!this._table.
|
|
601
|
+
// Ensure sort comparator is only configured if using the built-in in-memory datasource.
|
|
602
|
+
if (!this._table.isInMemoryDatasource() && typeof descriptor.sortable === 'object') {
|
|
300
603
|
throw Error('[ColumnDefinitionError] Configuring a sort comparator is not supported for tables using a datasource. Sorting must be done by the datasource.');
|
|
301
604
|
}
|
|
302
605
|
return {
|
|
@@ -342,353 +645,69 @@ function coerceColumnDescriptor(argument1, argument2) {
|
|
|
342
645
|
*
|
|
343
646
|
* SPDX-License-Identifier: EPL-2.0
|
|
344
647
|
*/
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
*/
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
648
|
+
class SciTableCache {
|
|
649
|
+
_entriesById = signal(new Map(), /* @ts-ignore */
|
|
650
|
+
...(ngDevMode ? [{ debugName: "_entriesById" }] : /* istanbul ignore next */ []));
|
|
651
|
+
_idsByEntry = computed(() => [...this._entriesById().entries()].reduce((map, [id, entry]) => {
|
|
652
|
+
const ids = map.get(entry) ?? new Set();
|
|
653
|
+
ids.add(id);
|
|
654
|
+
return map.set(entry, ids);
|
|
655
|
+
}, new Map()), /* @ts-ignore */
|
|
656
|
+
...(ngDevMode ? [{ debugName: "_idsByEntry" }] : /* istanbul ignore next */ []));
|
|
657
|
+
rowsById = computed(() => this._entries()
|
|
658
|
+
.flatMap(entry => entry.status() === 'error' ? [] : entry.value())
|
|
659
|
+
.reduce((acc, row) => row.id !== skeletonRowId ? acc.set(row.id, row) : acc, new Map()), { ...(ngDevMode ? { debugName: "rowsById" } : /* istanbul ignore next */ {}), equal: Objects.isEqual });
|
|
660
|
+
_entries = computed(() => [...new Set(this._entriesById().values())], /* @ts-ignore */
|
|
661
|
+
...(ngDevMode ? [{ debugName: "_entries" }] : /* istanbul ignore next */ []));
|
|
662
|
+
loading = computed(() => this._entries().some(entry => entry.isLoading()), /* @ts-ignore */
|
|
663
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
664
|
+
error = computed(() => this._entries().some(entry => entry.status() === 'error'), /* @ts-ignore */
|
|
665
|
+
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
666
|
+
has(key) {
|
|
667
|
+
return this._entriesById().has(key);
|
|
352
668
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
* available under the terms of the Eclipse Public License 2.0
|
|
361
|
-
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
362
|
-
*
|
|
363
|
-
* SPDX-License-Identifier: EPL-2.0
|
|
364
|
-
*/
|
|
365
|
-
const SCI_TABLE_STORAGE = new InjectionToken('SCI_TABLE_STORAGE', { factory: () => ({
|
|
366
|
-
load: (name) => localStorage.getItem(name),
|
|
367
|
-
store: (name, value) => localStorage.setItem(name, value),
|
|
368
|
-
}) });
|
|
369
|
-
/**
|
|
370
|
-
* Defaults to localStorage persistence.
|
|
371
|
-
* @param storage
|
|
372
|
-
*/
|
|
373
|
-
function provideTableStorage(storage) {
|
|
374
|
-
return makeEnvironmentProviders([
|
|
375
|
-
{ provide: SCI_TABLE_STORAGE, useClass: storage },
|
|
376
|
-
]);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/** @inheritDoc */
|
|
380
|
-
class ɵSciTableCellValuePreloader {
|
|
381
|
-
_executor = new LatestTaskExecutor();
|
|
382
|
-
_queue = new Array(); // PERF: Do not change to signal to avoid array re-allocation on enqueue (expensive); queue size is tracked via a separate signal.
|
|
383
|
-
_queueSize = signal(0, /* @ts-ignore */
|
|
384
|
-
...(ngDevMode ? [{ debugName: "_queueSize" }] : /* istanbul ignore next */ []));
|
|
385
|
-
_injector = inject(Injector);
|
|
386
|
-
/** @inheritDoc */
|
|
387
|
-
loading = computed(() => this._queueSize() > 0, /* @ts-ignore */
|
|
388
|
-
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
389
|
-
constructor() {
|
|
390
|
-
inject(DestroyRef).onDestroy(() => {
|
|
391
|
-
this._executor.destroy();
|
|
392
|
-
this._queue.length = 0;
|
|
393
|
-
this._queueSize.set(0);
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
/** @inheritDoc */
|
|
397
|
-
queue(cellValueFn) {
|
|
398
|
-
this._queue.push(cellValueFn);
|
|
399
|
-
this._queueSize.update(size => size + 1);
|
|
400
|
-
this._executor.submit(() => this.whenIdle().then(() => this.preloadQueuedBatch(PRELOAD_BATCH_SIZE)));
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Preloads the next batch of queued cell values, FIFO, then, if the queue is not empty, schedules the next batch.
|
|
404
|
-
*/
|
|
405
|
-
async preloadQueuedBatch(batchSize) {
|
|
406
|
-
this._queue.splice(0, batchSize).forEach(fn => fn());
|
|
407
|
-
this._queueSize.set(this._queue.length);
|
|
408
|
-
// Preload next batch when idle again.
|
|
409
|
-
if (this._queue.length) {
|
|
410
|
-
this._executor.submit(() => this.whenIdle().then(() => this.preloadQueuedBatch(PRELOAD_BATCH_SIZE)));
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
/**
|
|
414
|
-
* Resolves when the browser becomes idle.
|
|
415
|
-
*
|
|
416
|
-
* @see onIdle
|
|
417
|
-
*/
|
|
418
|
-
whenIdle() {
|
|
419
|
-
return runInInjectionContext(this._injector, onIdle);
|
|
420
|
-
}
|
|
421
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ɵSciTableCellValuePreloader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
422
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ɵSciTableCellValuePreloader });
|
|
423
|
-
}
|
|
424
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: ɵSciTableCellValuePreloader, decorators: [{
|
|
425
|
-
type: Injectable
|
|
426
|
-
}], ctorParameters: () => [] });
|
|
427
|
-
const PRELOAD_BATCH_SIZE = 50_000;
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Preloads cell values in the background when the browser is idle. This prevents lag with large datasets, in particular
|
|
431
|
-
* for expensive computations like formatting dates and generating ISO 8601 strings, providing faster sort and filter performance.
|
|
432
|
-
*
|
|
433
|
-
* If a cell's value is requested before background preloading finishes, it is computed synchronously.
|
|
434
|
-
*/
|
|
435
|
-
class SciTableCellValuePreloader {
|
|
436
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableCellValuePreloader, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
437
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableCellValuePreloader, providedIn: 'root', useClass: ɵSciTableCellValuePreloader });
|
|
438
|
-
}
|
|
439
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableCellValuePreloader, decorators: [{
|
|
440
|
-
type: Injectable,
|
|
441
|
-
args: [{ providedIn: 'root', useClass: ɵSciTableCellValuePreloader }]
|
|
442
|
-
}] });
|
|
443
|
-
|
|
444
|
-
/*
|
|
445
|
-
* Copyright (c) 2018-2026 Swiss Federal Railways
|
|
446
|
-
*
|
|
447
|
-
* This program and the accompanying materials are made
|
|
448
|
-
* available under the terms of the Eclipse Public License 2.0
|
|
449
|
-
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
450
|
-
*
|
|
451
|
-
* SPDX-License-Identifier: EPL-2.0
|
|
452
|
-
*/
|
|
453
|
-
function arrayDatasource(data, columns) {
|
|
454
|
-
const dataset = new Dataset(data, columns);
|
|
455
|
-
return markAsArrayDatasource((request) => {
|
|
456
|
-
dataset.columnFilters.set(request.columnFilters);
|
|
457
|
-
dataset.tableFilter.set(request.tableFilter);
|
|
458
|
-
dataset.sortCriteria.set(request.sortCriteria);
|
|
459
|
-
const totalCount = dataset.count;
|
|
460
|
-
const items = dataset.slice(request.start, request.end);
|
|
461
|
-
return toObservable(computed(() => ({
|
|
462
|
-
totalCount: totalCount(),
|
|
463
|
-
items: items(),
|
|
464
|
-
})));
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Provides a filtered and sorted view on given data.
|
|
469
|
-
*/
|
|
470
|
-
class Dataset {
|
|
471
|
-
_dataview;
|
|
472
|
-
columnFilters = signal([], { ...(ngDevMode ? { debugName: "columnFilters" } : /* istanbul ignore next */ {}), equal: Objects.isEqual });
|
|
473
|
-
sortCriteria = signal([], { ...(ngDevMode ? { debugName: "sortCriteria" } : /* istanbul ignore next */ {}), equal: Objects.isEqual });
|
|
474
|
-
tableFilter = signal(undefined, /* @ts-ignore */
|
|
475
|
-
...(ngDevMode ? [{ debugName: "tableFilter" }] : /* istanbul ignore next */ []));
|
|
476
|
-
/**
|
|
477
|
-
* Returns the total count of items matching the current filters and search criteria.
|
|
478
|
-
*/
|
|
479
|
-
count = computed(() => this._dataview().length, /* @ts-ignore */
|
|
480
|
-
...(ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []));
|
|
481
|
-
constructor(data, columns) {
|
|
482
|
-
const tableCellValuePreloader = inject(SciTableCellValuePreloader);
|
|
483
|
-
// Create a factory to instantiate a new injector per dataset run, releasing resources allocated in column value functions.
|
|
484
|
-
const injectorFactory = new _SingleActiveInjectorFactory();
|
|
485
|
-
const dataset = computed(() => {
|
|
486
|
-
const injector = injectorFactory.create();
|
|
487
|
-
return data().map(item => ({
|
|
488
|
-
item,
|
|
489
|
-
cells: columns().reduce((cells, column) => untracked(() => {
|
|
490
|
-
// PERF: Cell values are computed lazily (on idle or first use) to defer expensive computations like date formatting.
|
|
491
|
-
const tableCellValue = 'value' in column ? new eSciTableCellValue(tableCellValuePreloader, () => runInInjectionContext(injector, () => column.value(item)())) : undefined;
|
|
492
|
-
return cells.set(column.name, {
|
|
493
|
-
column: column,
|
|
494
|
-
value: () => tableCellValue?.value(),
|
|
495
|
-
});
|
|
496
|
-
}), new Map()),
|
|
497
|
-
}));
|
|
498
|
-
}, /* @ts-ignore */
|
|
499
|
-
...(ngDevMode ? [{ debugName: "dataset" }] : /* istanbul ignore next */ []));
|
|
500
|
-
// Memoize filtered/sorted view; recomputes only when dataset or criteria change, not when scrolling through the view, as sorting is an expensive operation.
|
|
501
|
-
this._dataview = computed(() => {
|
|
502
|
-
const rows = dataset();
|
|
503
|
-
const columnFilters = this.columnFilters();
|
|
504
|
-
const tableFilter = this.tableFilter();
|
|
505
|
-
const sortCriteria = this.sortCriteria();
|
|
506
|
-
// PERF: Do not track signals inside `Array.sort` to avoid Angular signal tracking overhead as the comparator runs repeatedly, degrading performance otherwise.
|
|
507
|
-
return untracked(() => rows
|
|
508
|
-
.filter(row => matchesRow(row, columnFilters, { coerceEnumValueToArray: true }) && matchesGlobalFilter(row, tableFilter, { coerceEnumValueToArray: false }))
|
|
509
|
-
.sort((a, b) => compareRows(a, b, sortCriteria))
|
|
510
|
-
.map(row => row.item));
|
|
511
|
-
}, /* @ts-ignore */
|
|
512
|
-
...(ngDevMode ? [{ debugName: "_dataview" }] : /* istanbul ignore next */ []));
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* Returns a slice of the filtered and sorted data within the given range (start inclusive, end exclusive).
|
|
516
|
-
*/
|
|
517
|
-
slice(start, end) {
|
|
518
|
-
return computed(() => this._dataview().slice(start, end));
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
/**
|
|
522
|
-
* Tests whether a row matches the given global filter.
|
|
523
|
-
*/
|
|
524
|
-
function matchesGlobalFilter(row, filter, options) {
|
|
525
|
-
if (!filter?.trim()) {
|
|
526
|
-
return true;
|
|
527
|
-
}
|
|
528
|
-
for (const cell of row.cells.values()) {
|
|
529
|
-
const text = cell.column.type === 'number' ? toNumber(filter) : filter;
|
|
530
|
-
if (text === undefined) {
|
|
531
|
-
continue;
|
|
532
|
-
}
|
|
533
|
-
if (matchesRow(row, [{ text, columnName: cell.column.name }], options)) {
|
|
534
|
-
return true;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
return false;
|
|
538
|
-
}
|
|
539
|
-
/**
|
|
540
|
-
* Tests whether a row matches the given column filters.
|
|
541
|
-
*/
|
|
542
|
-
function matchesRow(row, columnFilters, options) {
|
|
543
|
-
if (!columnFilters.length) {
|
|
544
|
-
return true;
|
|
545
|
-
}
|
|
546
|
-
for (const columnFilter of columnFilters) {
|
|
547
|
-
const cell = row.cells.get(columnFilter.columnName);
|
|
548
|
-
if ('value' in cell.column && cell.value() === undefined) { // Component and template columns have no value.
|
|
549
|
-
return false;
|
|
550
|
-
}
|
|
551
|
-
const filter = cell.column.type === 'enum' && options.coerceEnumValueToArray ? Arrays.coerce(columnFilter.text) : columnFilter.text;
|
|
552
|
-
if (!cell.column.matches(filter, { item: row.item, value: cell.value() })) {
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return true;
|
|
557
|
-
}
|
|
558
|
-
/**
|
|
559
|
-
* Compares two rows based on the given sort criteria.
|
|
560
|
-
*/
|
|
561
|
-
function compareRows(row1, row2, sortCriteria) {
|
|
562
|
-
if (!sortCriteria.length) {
|
|
563
|
-
return 0;
|
|
564
|
-
}
|
|
565
|
-
for (const criterion of sortCriteria) {
|
|
566
|
-
const cell1 = row1.cells.get(criterion.columnName);
|
|
567
|
-
const cell2 = row2.cells.get(criterion.columnName);
|
|
568
|
-
const value1 = cell1.value();
|
|
569
|
-
const value2 = cell2.value();
|
|
570
|
-
// Always place `undefined` values at the end, independent of the sort direction. Note that component and template columns have no value.
|
|
571
|
-
if (value1 === undefined && value2 !== undefined) {
|
|
572
|
-
return 1;
|
|
573
|
-
}
|
|
574
|
-
if (value2 === undefined && value1 !== undefined) {
|
|
575
|
-
return -1;
|
|
576
|
-
}
|
|
577
|
-
// Prevent calling comparator for value columns where both values are undefined. Note that component and template columns have no value.
|
|
578
|
-
if (value1 === undefined && value2 === undefined && 'value' in cell1.column) {
|
|
579
|
-
return 0;
|
|
580
|
-
}
|
|
581
|
-
const comparison = cell1.column.compare({ item: row1.item, value: value1 }, { item: row2.item, value: value2 });
|
|
582
|
-
if (comparison !== 0) {
|
|
583
|
-
return criterion.direction === 'asc' ? comparison : -comparison;
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
return 0;
|
|
587
|
-
}
|
|
588
|
-
/**
|
|
589
|
-
* Checks whether given {@link SciTableDataLoaderFn} represents an {@link arrayDatasource}.
|
|
590
|
-
*/
|
|
591
|
-
function isArrayDatasource(loader) {
|
|
592
|
-
return ARRAY_DATASOURCE_MARKER in loader;
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* Marks given {@link SciTableDataLoaderFn} as an array data source.
|
|
596
|
-
*/
|
|
597
|
-
function markAsArrayDatasource(loader) {
|
|
598
|
-
Object.defineProperty(loader, ARRAY_DATASOURCE_MARKER, { value: true, writable: false, enumerable: false, configurable: false });
|
|
599
|
-
return loader;
|
|
600
|
-
}
|
|
601
|
-
/**
|
|
602
|
-
* Identifies a {@link SciTableDataLoaderFn} as an {@link arrayDatasource}.
|
|
603
|
-
*/
|
|
604
|
-
const ARRAY_DATASOURCE_MARKER = Symbol('ARRAY_DATASOURCE_HINT');
|
|
605
|
-
/**
|
|
606
|
-
* Converts the given string to a number, or `undefined` if it cannot be parsed.
|
|
607
|
-
*/
|
|
608
|
-
function toNumber(numberLike) {
|
|
609
|
-
const number = Number.parseFloat(numberLike);
|
|
610
|
-
return !Number.isNaN(number) ? number : undefined;
|
|
611
|
-
}
|
|
612
|
-
/**
|
|
613
|
-
* Provides lazy loading for the cell's value, preloading it when idle for better sort/filter performance.
|
|
614
|
-
*
|
|
615
|
-
* The value is computed at most once, whether it gets preloaded during idle time or requested synchronously via {@link value()}.
|
|
616
|
-
*/
|
|
617
|
-
class eSciTableCellValue {
|
|
618
|
-
_valueFn;
|
|
619
|
-
_value;
|
|
620
|
-
constructor(preloader, _valueFn) {
|
|
621
|
-
this._valueFn = _valueFn;
|
|
622
|
-
preloader.queue(() => {
|
|
623
|
-
this._value ??= this._valueFn();
|
|
669
|
+
set(ids, entry) {
|
|
670
|
+
this._entriesById.update(cache => {
|
|
671
|
+
const newCache = new Map(cache);
|
|
672
|
+
for (const id of ids) {
|
|
673
|
+
newCache.set(id, entry);
|
|
674
|
+
}
|
|
675
|
+
return newCache;
|
|
624
676
|
});
|
|
625
677
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
/*
|
|
632
|
-
* Copyright (c) 2018-2026 Swiss Federal Railways
|
|
633
|
-
*
|
|
634
|
-
* This program and the accompanying materials are made
|
|
635
|
-
* available under the terms of the Eclipse Public License 2.0
|
|
636
|
-
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
|
637
|
-
*
|
|
638
|
-
* SPDX-License-Identifier: EPL-2.0
|
|
639
|
-
*/
|
|
640
|
-
class SciTableCache {
|
|
641
|
-
_cache = signal(new Map(), /* @ts-ignore */
|
|
642
|
-
...(ngDevMode ? [{ debugName: "_cache" }] : /* istanbul ignore next */ []));
|
|
643
|
-
loading = computed(() => this.values().some(entry => entry.rows.isLoading()), /* @ts-ignore */
|
|
644
|
-
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
645
|
-
error = computed(() => this.values().find(entry => entry.rows.error())?.rows.error(), /* @ts-ignore */
|
|
646
|
-
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
647
|
-
values = computed(() => [...this._cache().values()], /* @ts-ignore */
|
|
648
|
-
...(ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []));
|
|
649
|
-
rowsByIndex = computed(() => this.values()
|
|
650
|
-
.flatMap(page => page.rows.value())
|
|
651
|
-
.reduce((acc, row) => acc.set(row.index, row), new Map()), { ...(ngDevMode ? { debugName: "rowsByIndex" } : /* istanbul ignore next */ {}), equal: Objects.isEqual });
|
|
652
|
-
rowsById = computed(() => this.values()
|
|
653
|
-
.flatMap(page => page.rows.value())
|
|
654
|
-
.reduce((acc, row) => row.id !== undefined ? acc.set(row.id, row) : acc, new Map()), { ...(ngDevMode ? { debugName: "rowsById" } : /* istanbul ignore next */ {}), equal: Objects.isEqual });
|
|
655
|
-
has(key) {
|
|
656
|
-
return this._cache().has(key);
|
|
657
|
-
}
|
|
658
|
-
get(key) {
|
|
659
|
-
return this._cache().get(key);
|
|
660
|
-
}
|
|
661
|
-
set(key, entry) {
|
|
662
|
-
this._cache.update(cache => {
|
|
678
|
+
deleteIfLoading(ids) {
|
|
679
|
+
this._entriesById.update(cache => {
|
|
663
680
|
const cacheCopy = new Map(cache);
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
cacheCopy.
|
|
667
|
-
|
|
681
|
+
const entries = new Set(ids.map(id => cacheCopy.get(id)));
|
|
682
|
+
for (const id of ids) {
|
|
683
|
+
if (cacheCopy.get(id)?.isLoading()) {
|
|
684
|
+
cacheCopy.delete(id);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// Dispose entries, which have no ids left pointing to them.
|
|
688
|
+
for (const entry of entries) {
|
|
689
|
+
if (!entry) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const idsByEntry = this._idsByEntry().get(entry);
|
|
693
|
+
if (entry.isLoading() && [...(idsByEntry ?? [])].every(id => ids.includes(id))) {
|
|
694
|
+
entry.destroy();
|
|
695
|
+
}
|
|
668
696
|
}
|
|
669
|
-
cacheCopy.set(key, entry);
|
|
670
697
|
return cacheCopy;
|
|
671
698
|
});
|
|
672
699
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
if (!cacheEntry?.rows.isLoading()) {
|
|
679
|
-
return;
|
|
700
|
+
reloadFailed() {
|
|
701
|
+
for (const entry of this._entries()) {
|
|
702
|
+
if (entry.status() === 'error') {
|
|
703
|
+
entry.reload();
|
|
704
|
+
}
|
|
680
705
|
}
|
|
681
|
-
this._cache.update(cache => {
|
|
682
|
-
const cacheCopy = new Map(cache);
|
|
683
|
-
cacheCopy.delete(key);
|
|
684
|
-
cacheEntry.dispose();
|
|
685
|
-
return cacheCopy;
|
|
686
|
-
});
|
|
687
706
|
}
|
|
688
707
|
clear() {
|
|
689
|
-
this.
|
|
690
|
-
for (const entry of cache.values()) {
|
|
691
|
-
entry.
|
|
708
|
+
this._entriesById.update(cache => {
|
|
709
|
+
for (const entry of new Set(cache.values())) {
|
|
710
|
+
entry.destroy();
|
|
692
711
|
}
|
|
693
712
|
return new Map();
|
|
694
713
|
});
|
|
@@ -782,6 +801,327 @@ const SCI_TABLE_ROW_BINDING = new InjectionToken('SCI_TABLE_ROW_BINDING');
|
|
|
782
801
|
*/
|
|
783
802
|
const SCI_TABLE_ROW_BINDINGS = new InjectionToken('SCI_TABLE_ROW_BINDINGS');
|
|
784
803
|
|
|
804
|
+
/**
|
|
805
|
+
* Loads and builds the table's hierarchy using item IDs and parent relationships, without loading full items.
|
|
806
|
+
* Only operates on IDs, never full objects and regardless of the viewport / virtual scrolling state.
|
|
807
|
+
* For non-hierarchical datasources, treats each item as a root node and preserves the order from the unfiltered response.
|
|
808
|
+
*
|
|
809
|
+
* Uses two independent requests:
|
|
810
|
+
* - An unfiltered request loads all nodes initially and whenever the sort criteria change.
|
|
811
|
+
* - A filtered request loads matching nodes when a filter is present and whenever the filters change.
|
|
812
|
+
*
|
|
813
|
+
* Combines both responses to include matching nodes and their parents and children, preserving the order from the unfiltered response.
|
|
814
|
+
* When no filter is present, uses the unfiltered response directly.
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* ```ts
|
|
818
|
+
* const unfilteredNodes = [
|
|
819
|
+
* {item: 'Downloads'},
|
|
820
|
+
* {item: 'Desktop'},
|
|
821
|
+
* {item: 'New Folder', parent: 'Desktop'},
|
|
822
|
+
* {item: 'notes.txt', parent: 'New Folder'},
|
|
823
|
+
* ];
|
|
824
|
+
*
|
|
825
|
+
* // Filtered by 'notes'
|
|
826
|
+
* const filteredNodes = [{item: 'notes.txt', parent: 'New Folder'}];
|
|
827
|
+
*
|
|
828
|
+
* // Result includes the matching node and its ancestors.
|
|
829
|
+
* const tree = [
|
|
830
|
+
* {item: 'Desktop'},
|
|
831
|
+
* {item: 'New Folder', parent: 'Desktop'},
|
|
832
|
+
* {item: 'notes.txt', parent: 'New Folder'},
|
|
833
|
+
* ];
|
|
834
|
+
* ```
|
|
835
|
+
*/
|
|
836
|
+
class SciTableTree {
|
|
837
|
+
_injector = inject(Injector);
|
|
838
|
+
_datasource;
|
|
839
|
+
_sortCriteria;
|
|
840
|
+
_columnFilters;
|
|
841
|
+
_tableFilter;
|
|
842
|
+
_activeNode = this.computeActiveNode();
|
|
843
|
+
_selectedNodes = this.computeSelectedNodes();
|
|
844
|
+
_expandedNodes = this.computeExpandedNodes();
|
|
845
|
+
_unfilteredNodes = this.computeUnfilteredNodes();
|
|
846
|
+
_filteredNodes = this.computeFilteredNodes();
|
|
847
|
+
_tree = this.computeTree();
|
|
848
|
+
expandedNodes = this._expandedNodes.asReadonly();
|
|
849
|
+
selectedNodes = this._selectedNodes.asReadonly();
|
|
850
|
+
activeNode = this._activeNode.asReadonly();
|
|
851
|
+
error = computed(() => this._unfilteredNodes.status() === 'error' || this._filteredNodes.status() === 'error', /* @ts-ignore */
|
|
852
|
+
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
853
|
+
loading = computed(() => this._unfilteredNodes.isLoading() || this._filteredNodes.isLoading(), /* @ts-ignore */
|
|
854
|
+
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
855
|
+
unfilteredNodesById = computed(() => this._unfilteredNodes.status() === 'error' ? new Map() : this._unfilteredNodes.value().reduce((acc, node) => acc.set(node.id, node), new Map()), /* @ts-ignore */
|
|
856
|
+
...(ngDevMode ? [{ debugName: "unfilteredNodesById" }] : /* istanbul ignore next */ []));
|
|
857
|
+
nodes = computed(() => this._tree().nodes, /* @ts-ignore */
|
|
858
|
+
...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
|
|
859
|
+
nodesById = computed(() => this._tree().nodesById, /* @ts-ignore */
|
|
860
|
+
...(ngDevMode ? [{ debugName: "nodesById" }] : /* istanbul ignore next */ []));
|
|
861
|
+
childrenIdsByParent = computed(() => this._tree().childrenIdsByParent, /* @ts-ignore */
|
|
862
|
+
...(ngDevMode ? [{ debugName: "childrenIdsByParent" }] : /* istanbul ignore next */ []));
|
|
863
|
+
visibleNodes = this.computeVisibleNodes();
|
|
864
|
+
nodeIndexById = computed(() => this.visibleNodes().reduce((map, node, index) => map.set(node.id, index), new Map()), /* @ts-ignore */
|
|
865
|
+
...(ngDevMode ? [{ debugName: "nodeIndexById" }] : /* istanbul ignore next */ []));
|
|
866
|
+
visibleNodeCount = computed(() => this.visibleNodes().length, /* @ts-ignore */
|
|
867
|
+
...(ngDevMode ? [{ debugName: "visibleNodeCount" }] : /* istanbul ignore next */ []));
|
|
868
|
+
constructor(options) {
|
|
869
|
+
this._datasource = options.datasource;
|
|
870
|
+
this._columnFilters = options.columnFilters;
|
|
871
|
+
this._tableFilter = options.tableFilter;
|
|
872
|
+
this._sortCriteria = options.sortCriteria;
|
|
873
|
+
}
|
|
874
|
+
reload() {
|
|
875
|
+
this._unfilteredNodes.reload();
|
|
876
|
+
}
|
|
877
|
+
reloadFailed() {
|
|
878
|
+
if (this._unfilteredNodes.status() === 'error') {
|
|
879
|
+
this._unfilteredNodes.reload();
|
|
880
|
+
}
|
|
881
|
+
if (this._filteredNodes.status() === 'error') {
|
|
882
|
+
this._filteredNodes.reload();
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
expand(id) {
|
|
886
|
+
const ids = Arrays.coerce(id);
|
|
887
|
+
this._expandedNodes.update(toggledRows => new Set([...toggledRows, ...ids]));
|
|
888
|
+
}
|
|
889
|
+
expandAll() {
|
|
890
|
+
this._expandedNodes.set(new Set([...this.childrenIdsByParent().keys()].filter((id) => id !== undefined)));
|
|
891
|
+
}
|
|
892
|
+
collapse(id) {
|
|
893
|
+
const ids = Arrays.coerce(id);
|
|
894
|
+
this._expandedNodes.update(toggledRows => {
|
|
895
|
+
const newToggledRows = new Set(toggledRows);
|
|
896
|
+
ids.forEach(id => newToggledRows.delete(id));
|
|
897
|
+
return newToggledRows;
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
collapseAll() {
|
|
901
|
+
this._expandedNodes.set(new Set());
|
|
902
|
+
}
|
|
903
|
+
toggleRow(id) {
|
|
904
|
+
this._expandedNodes.update(toggledRows => {
|
|
905
|
+
if (toggledRows.has(id)) {
|
|
906
|
+
const newToggledRows = new Set(toggledRows);
|
|
907
|
+
newToggledRows.delete(id);
|
|
908
|
+
return newToggledRows;
|
|
909
|
+
}
|
|
910
|
+
else {
|
|
911
|
+
return new Set(toggledRows).add(id);
|
|
912
|
+
}
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
setActiveNode(id) {
|
|
916
|
+
this._activeNode.set(id);
|
|
917
|
+
}
|
|
918
|
+
updateSelectedNodes(updateFn) {
|
|
919
|
+
this._selectedNodes.update(updateFn);
|
|
920
|
+
}
|
|
921
|
+
select(ids) {
|
|
922
|
+
// Prevents selecting nodes which do not exist.
|
|
923
|
+
// Check against unfiltered nodes, to allow selecting nodes, which are filtered out.
|
|
924
|
+
const nodesById = this.unfilteredNodesById();
|
|
925
|
+
const nodeIds = Arrays.coerce(ids).filter(id => nodesById.has(id));
|
|
926
|
+
// Expand nodes.
|
|
927
|
+
nodeIds.forEach(id => {
|
|
928
|
+
const node = nodesById.get(id);
|
|
929
|
+
if (node?.parentId !== undefined && nodesById.has(node.parentId)) {
|
|
930
|
+
// Expand all nodes before the selected ones.
|
|
931
|
+
this.expand(getPathFromRoot(nodesById.get(node.parentId), nodesById));
|
|
932
|
+
}
|
|
933
|
+
});
|
|
934
|
+
// Wait for viewport height to update after expanding nodes.
|
|
935
|
+
// Else scrollActiveRowIntoViewport operates on an old viewport height.
|
|
936
|
+
requestAnimationFrame(() => {
|
|
937
|
+
this._selectedNodes.set(new Set(nodeIds));
|
|
938
|
+
// Find and activate last item based on the visual index.
|
|
939
|
+
const lastItemIndex = Math.max(...nodeIds.map(id => this.nodeIndexById().get(id) ?? -1));
|
|
940
|
+
const lastItem = this.visibleNodes()[lastItemIndex]?.id;
|
|
941
|
+
this._activeNode.set(lastItem);
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
getIds$(query, options) {
|
|
945
|
+
return runInInjectionContext(options.injector, () => Observables.coerce(this._datasource.getTreeTableNodes({
|
|
946
|
+
columnFilters: query.columnFilters ?? [],
|
|
947
|
+
sortCriteria: query.sortCriteria ?? [],
|
|
948
|
+
tableFilter: query.tableFilter,
|
|
949
|
+
})).pipe(catchError(error => {
|
|
950
|
+
console.error(error);
|
|
951
|
+
throw error;
|
|
952
|
+
})));
|
|
953
|
+
}
|
|
954
|
+
computeUnfilteredNodes() {
|
|
955
|
+
// Create separate injection context per resource invocation to clean up allocated resources.
|
|
956
|
+
const injectorFactory = new _SingleActiveInjectorFactory({ parent: this._injector });
|
|
957
|
+
return rxResource({
|
|
958
|
+
params: () => this._sortCriteria(),
|
|
959
|
+
stream: ({ params: sortCriteria }) => this.getIds$({ sortCriteria }, { injector: injectorFactory.create() }),
|
|
960
|
+
defaultValue: [],
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
computeFilteredNodes() {
|
|
964
|
+
// Create separate injection context per resource invocation to clean up allocated resources.
|
|
965
|
+
const injectorFactory = new _SingleActiveInjectorFactory({ parent: this._injector });
|
|
966
|
+
return rxResource({
|
|
967
|
+
params: () => ({
|
|
968
|
+
columnFilters: this._columnFilters(),
|
|
969
|
+
tableFilter: this._tableFilter() ?? undefined,
|
|
970
|
+
}),
|
|
971
|
+
stream: ({ params }) => {
|
|
972
|
+
const injector = injectorFactory.create();
|
|
973
|
+
if (params.columnFilters.length === 0 && params.tableFilter === undefined) {
|
|
974
|
+
return runInInjectionContext(injector, () => toObservable(this._unfilteredNodes.value));
|
|
975
|
+
}
|
|
976
|
+
return this.getIds$({ columnFilters: params.columnFilters, tableFilter: params.tableFilter }, { injector });
|
|
977
|
+
},
|
|
978
|
+
defaultValue: [],
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
computeTree() {
|
|
982
|
+
return computed(() => {
|
|
983
|
+
if (this.error()) {
|
|
984
|
+
return {
|
|
985
|
+
nodes: [],
|
|
986
|
+
nodesById: new Map(),
|
|
987
|
+
childrenIdsByParent: new Map(),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const filteredNodes = this._filteredNodes.value();
|
|
991
|
+
const unfilteredNodes = this._unfilteredNodes.value();
|
|
992
|
+
return untracked(() => createTree(filteredNodes, unfilteredNodes));
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
computeExpandedNodes() {
|
|
996
|
+
return linkedSignal({
|
|
997
|
+
source: () => ({ unfilteredNodesById: this.unfilteredNodesById(), loading: this._unfilteredNodes.isLoading(), columnFilters: this._columnFilters(), tableFilter: this._tableFilter() }),
|
|
998
|
+
computation: ({ unfilteredNodesById, loading, tableFilter, columnFilters }, previous) => {
|
|
999
|
+
// Wait until resource finished loading before changing the expanded nodes.
|
|
1000
|
+
if (loading) {
|
|
1001
|
+
return previous?.value ?? new Set();
|
|
1002
|
+
}
|
|
1003
|
+
// Expand all items if filter is set.
|
|
1004
|
+
if (columnFilters.length > 0 || tableFilter?.trim()) {
|
|
1005
|
+
return new Set(unfilteredNodesById.keys());
|
|
1006
|
+
}
|
|
1007
|
+
const expandedNodeIds = previous?.value ?? new Set();
|
|
1008
|
+
return new Set([...expandedNodeIds].filter(id => unfilteredNodesById.has(id)));
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
computeSelectedNodes() {
|
|
1013
|
+
return linkedSignal({
|
|
1014
|
+
source: () => ({ unfilteredNodesById: this.unfilteredNodesById(), loading: this._unfilteredNodes.isLoading() }),
|
|
1015
|
+
computation: ({ unfilteredNodesById, loading }, previous) => {
|
|
1016
|
+
// Wait until resource finished loading before changing the selected nodes.
|
|
1017
|
+
if (loading) {
|
|
1018
|
+
return previous?.value ?? new Set();
|
|
1019
|
+
}
|
|
1020
|
+
const selectedNodeIds = previous?.value ?? new Set();
|
|
1021
|
+
return new Set([...selectedNodeIds].filter(id => unfilteredNodesById.has(id)));
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
computeActiveNode() {
|
|
1026
|
+
return linkedSignal({
|
|
1027
|
+
source: () => ({ unfilteredNodesById: this.unfilteredNodesById(), loading: this._unfilteredNodes.isLoading() }),
|
|
1028
|
+
computation: ({ unfilteredNodesById, loading }, previous) => {
|
|
1029
|
+
// Wait until resource finished loading before changing the active nodes.
|
|
1030
|
+
if (loading) {
|
|
1031
|
+
return previous?.value;
|
|
1032
|
+
}
|
|
1033
|
+
return previous?.value !== undefined && unfilteredNodesById.has(previous.value) ? previous.value : undefined;
|
|
1034
|
+
},
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
computeVisibleNodes() {
|
|
1038
|
+
return computed(() => {
|
|
1039
|
+
if (this.error()) {
|
|
1040
|
+
return [];
|
|
1041
|
+
}
|
|
1042
|
+
const expandedNodes = this._expandedNodes();
|
|
1043
|
+
const nodesById = this.nodesById();
|
|
1044
|
+
const nodes = this.nodes();
|
|
1045
|
+
const allAncestorsExpanded = (node) => {
|
|
1046
|
+
if (node?.parentId === undefined) {
|
|
1047
|
+
return true;
|
|
1048
|
+
}
|
|
1049
|
+
if (!expandedNodes.has(node.parentId)) {
|
|
1050
|
+
return false;
|
|
1051
|
+
}
|
|
1052
|
+
return allAncestorsExpanded(nodesById.get(node.parentId));
|
|
1053
|
+
};
|
|
1054
|
+
return untracked(() => nodes.filter(allAncestorsExpanded));
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function createTree(filteredNodes, unfilteredNodes) {
|
|
1059
|
+
const nodesById = includeParentsAndChildren(unfilteredNodes, filteredNodes);
|
|
1060
|
+
const sortedNodes = unfilteredNodes.filter(node => nodesById.has(node.id));
|
|
1061
|
+
const childrenIdsByParent = groupChildrenByParent(sortedNodes);
|
|
1062
|
+
const nodes = flattenTree(nodesById, childrenIdsByParent);
|
|
1063
|
+
return ({ nodes, nodesById, childrenIdsByParent });
|
|
1064
|
+
}
|
|
1065
|
+
function includeParentsAndChildren(unfilteredNodes, filteredNodes) {
|
|
1066
|
+
const nodesById = new Map(unfilteredNodes.map(node => [node.id, node]));
|
|
1067
|
+
const filteredIds = new Set(filteredNodes.map(node => node.id));
|
|
1068
|
+
return unfilteredNodes.reduce((acc, node) => {
|
|
1069
|
+
const path = getPathFromRoot(node, nodesById);
|
|
1070
|
+
// Include the full path (parents and children) of any filtered node.
|
|
1071
|
+
// This ensures that all parents and children are displayed, even if they don't match the filter.
|
|
1072
|
+
if (path.some(id => filteredIds.has(id))) {
|
|
1073
|
+
path.forEach(id => acc.set(id, nodesById.get(id)));
|
|
1074
|
+
}
|
|
1075
|
+
return acc;
|
|
1076
|
+
}, new Map());
|
|
1077
|
+
}
|
|
1078
|
+
function groupChildrenByParent(nodes) {
|
|
1079
|
+
return nodes.reduce((acc, node) => {
|
|
1080
|
+
const values = acc.get(node.parentId) ?? [];
|
|
1081
|
+
values.push(node.id);
|
|
1082
|
+
return acc.set(node.parentId, values);
|
|
1083
|
+
}, new Map());
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Flattens the tree in depth-first order, placing each parent before its descendants and preserving sibling order.
|
|
1087
|
+
* Includes all nodes regardless of expansion state.
|
|
1088
|
+
*
|
|
1089
|
+
* Example:
|
|
1090
|
+
* ```
|
|
1091
|
+
* [
|
|
1092
|
+
* {id: '1'},
|
|
1093
|
+
* {id: '1.1', parentId: '1'},
|
|
1094
|
+
* {id: '1.2', parentId: '1'},
|
|
1095
|
+
* {id: '1.2.1', parentId: '1.2'},
|
|
1096
|
+
* {id: '1.3', parentId: '1'},
|
|
1097
|
+
* {id: '2'},
|
|
1098
|
+
* ]
|
|
1099
|
+
* ```
|
|
1100
|
+
*/
|
|
1101
|
+
function flattenTree(nodesById, childrenIdsByParent) {
|
|
1102
|
+
const nodes = [];
|
|
1103
|
+
const roots = childrenIdsByParent.get(undefined) ?? [];
|
|
1104
|
+
const stack = roots.slice().reverse();
|
|
1105
|
+
// Don't use recursion, to avoid stack overflow on large amount of root nodes (e.g. flat table).
|
|
1106
|
+
while (stack.length > 0) {
|
|
1107
|
+
const id = stack.pop();
|
|
1108
|
+
nodes.push(nodesById.get(id));
|
|
1109
|
+
// Push children in reverse order so the first child is visited next.
|
|
1110
|
+
const children = childrenIdsByParent.get(id) ?? [];
|
|
1111
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
1112
|
+
if (children[i] !== undefined) {
|
|
1113
|
+
stack.push(children[i]);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return nodes;
|
|
1118
|
+
}
|
|
1119
|
+
function getPathFromRoot(node, nodesById) {
|
|
1120
|
+
const parent = node.parentId !== undefined ? nodesById.get(node.parentId) : undefined;
|
|
1121
|
+
const pathFromRoot = parent ? getPathFromRoot(parent, nodesById) : [];
|
|
1122
|
+
return pathFromRoot.concat(node.id);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
785
1125
|
/*
|
|
786
1126
|
* Copyright (c) 2018-2026 Swiss Federal Railways
|
|
787
1127
|
*
|
|
@@ -791,19 +1131,23 @@ const SCI_TABLE_ROW_BINDINGS = new InjectionToken('SCI_TABLE_ROW_BINDINGS');
|
|
|
791
1131
|
*
|
|
792
1132
|
* SPDX-License-Identifier: EPL-2.0
|
|
793
1133
|
*/
|
|
1134
|
+
/**
|
|
1135
|
+
* @docs-private Not public API. For internal use only.
|
|
1136
|
+
*/
|
|
794
1137
|
class ɵSciTable {
|
|
795
|
-
_tableStorage = inject(
|
|
1138
|
+
_tableStorage = inject(SCI_COMPONENTS_STORAGE);
|
|
796
1139
|
_injector = inject(Injector);
|
|
797
1140
|
name = signal(undefined, /* @ts-ignore */
|
|
798
1141
|
...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
|
|
799
1142
|
columns;
|
|
800
1143
|
rowActions;
|
|
801
1144
|
_rowBindings;
|
|
802
|
-
|
|
1145
|
+
_datasource;
|
|
803
1146
|
_trackBy;
|
|
804
1147
|
tableViewRef = signal(undefined, /* @ts-ignore */
|
|
805
1148
|
...(ngDevMode ? [{ debugName: "tableViewRef" }] : /* istanbul ignore next */ []));
|
|
806
1149
|
userSettings;
|
|
1150
|
+
type;
|
|
807
1151
|
bufferSize;
|
|
808
1152
|
pageSize;
|
|
809
1153
|
filterable;
|
|
@@ -822,42 +1166,38 @@ class ɵSciTable {
|
|
|
822
1166
|
sortCriteria = linkedSignal({ ...(ngDevMode ? { debugName: "sortCriteria" } : /* istanbul ignore next */ {}), source: () => this.columns(), // Discard stale criteria on column change.
|
|
823
1167
|
computation: (columns, previous) => previous?.value.filter(criteria => columns.some(column => column.name === criteria.columnName)) ?? [],
|
|
824
1168
|
equal: Objects.isEqual });
|
|
825
|
-
|
|
1169
|
+
columnFilters = linkedSignal({ ...(ngDevMode ? { debugName: "columnFilters" } : /* istanbul ignore next */ {}), source: () => this.columns(), // Discard stale criteria on column change.
|
|
826
1170
|
computation: (columns, previous) => previous?.value.filter(criteria => columns.some(column => column.name === criteria.columnName)) ?? [],
|
|
827
1171
|
equal: Objects.isEqual });
|
|
828
1172
|
_cache = new SciTableCache();
|
|
829
1173
|
_tableCellValuePreloader = inject(SciTableCellValuePreloader);
|
|
830
1174
|
_tableFilter = signal(null, /* @ts-ignore */
|
|
831
1175
|
...(ngDevMode ? [{ debugName: "_tableFilter" }] : /* istanbul ignore next */ []));
|
|
832
|
-
|
|
833
|
-
...(ngDevMode ? [{ debugName: "_selectedItems" }] : /* istanbul ignore next */ []));
|
|
834
|
-
// Reset total count on criteria change to show skeletons instead of stale data while loading.
|
|
835
|
-
totalCount = linkedSignal({ ...(ngDevMode ? { debugName: "totalCount" } : /* istanbul ignore next */ {}), source: () => this.criteria(),
|
|
836
|
-
computation: () => undefined });
|
|
837
|
-
activeItem = linkedSignal({ ...(ngDevMode ? { debugName: "activeItem" } : /* istanbul ignore next */ {}), source: () => this.criteria(),
|
|
838
|
-
computation: () => undefined });
|
|
1176
|
+
_tableTree;
|
|
839
1177
|
hoveredIndex = linkedSignal({ ...(ngDevMode ? { debugName: "hoveredIndex" } : /* istanbul ignore next */ {}), source: () => {
|
|
840
1178
|
this.criteria(); // reset on criteria change
|
|
841
1179
|
this.scrolling(); // reset when start scrolling
|
|
842
1180
|
},
|
|
843
1181
|
computation: () => -1 });
|
|
844
|
-
criteria = computed(() => ({ sort: this.sortCriteria(), filter: this.
|
|
1182
|
+
criteria = computed(() => ({ sort: this.sortCriteria(), filter: this.columnFilters(), tableFilter: this._tableFilter() }), /* @ts-ignore */
|
|
845
1183
|
...(ngDevMode ? [{ debugName: "criteria" }] : /* istanbul ignore next */ []));
|
|
846
|
-
loading = computed(() => this._cache.loading() || this._tableCellValuePreloader.loading(), /* @ts-ignore */
|
|
1184
|
+
loading = computed(() => this._tableTree.loading() || this._cache.loading() || this._tableCellValuePreloader.loading(), /* @ts-ignore */
|
|
847
1185
|
...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
848
|
-
error = this._cache.error
|
|
1186
|
+
error = computed(() => this._cache.error() || this._tableTree.error(), /* @ts-ignore */
|
|
1187
|
+
...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
|
|
849
1188
|
activeRow;
|
|
850
|
-
|
|
851
|
-
...(ngDevMode ? [{ debugName: "hoveredRow" }] : /* istanbul ignore next */ []));
|
|
852
|
-
selectedItems = computed(() => [...this._selectedItems().values()], /* @ts-ignore */
|
|
1189
|
+
selectedItems = computed(() => [...this._tableTree.selectedNodes().values()], /* @ts-ignore */
|
|
853
1190
|
...(ngDevMode ? [{ debugName: "selectedItems" }] : /* istanbul ignore next */ []));
|
|
854
|
-
|
|
855
|
-
...(ngDevMode ? [{ debugName: "selectedIds" }] : /* istanbul ignore next */ []));
|
|
856
|
-
rowsByIndex = this._cache.rowsByIndex;
|
|
1191
|
+
rowsById = this._cache.rowsById;
|
|
857
1192
|
rows = this.computeRows();
|
|
1193
|
+
visibleRowCount;
|
|
1194
|
+
visibleRows;
|
|
1195
|
+
rowIndexById;
|
|
1196
|
+
activeItem;
|
|
1197
|
+
activeIndex = computed(() => this.activeItem() !== undefined ? (this.rowIndexById().get(this.activeItem()) ?? -1) : -1, /* @ts-ignore */
|
|
1198
|
+
...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
|
|
858
1199
|
constructor(descriptor) {
|
|
859
|
-
|
|
860
|
-
descriptor = { ...descriptor, ɵdatasource: descriptor.ɵdatasource ?? descriptor.datasource };
|
|
1200
|
+
this.type = descriptor.type ?? 'table';
|
|
861
1201
|
this.bufferSize = descriptor.bufferSize ?? 10;
|
|
862
1202
|
this.pageSize = descriptor.pageSize ?? 50;
|
|
863
1203
|
this.sortable = signal(descriptor.sortable ?? true, /* @ts-ignore */
|
|
@@ -886,8 +1226,18 @@ class ɵSciTable {
|
|
|
886
1226
|
...inject(SCI_TABLE_ROW_BINDINGS, { optional: true }) ?? [],
|
|
887
1227
|
];
|
|
888
1228
|
this._trackBy = descriptor.trackBy;
|
|
889
|
-
|
|
890
|
-
this.
|
|
1229
|
+
const datasourceProvider = isSignal(descriptor.datasource) ? provideTableDatasource(descriptor.datasource) : descriptor.datasource;
|
|
1230
|
+
this._datasource = datasourceProvider[eSCI_TABLE_DATASOURCE_PROVIDER](this);
|
|
1231
|
+
this._tableTree = new SciTableTree({
|
|
1232
|
+
datasource: this._datasource,
|
|
1233
|
+
sortCriteria: this.sortCriteria,
|
|
1234
|
+
columnFilters: this.columnFilters,
|
|
1235
|
+
tableFilter: this._tableFilter,
|
|
1236
|
+
});
|
|
1237
|
+
this.visibleRowCount = this._tableTree.visibleNodeCount;
|
|
1238
|
+
this.rowIndexById = this._tableTree.nodeIndexById;
|
|
1239
|
+
this.visibleRows = this._tableTree.visibleNodes;
|
|
1240
|
+
this.activeItem = this._tableTree.activeNode;
|
|
891
1241
|
this.installPageLoader();
|
|
892
1242
|
}
|
|
893
1243
|
/**
|
|
@@ -919,15 +1269,29 @@ class ɵSciTable {
|
|
|
919
1269
|
if (!scrollRange) {
|
|
920
1270
|
return [];
|
|
921
1271
|
}
|
|
922
|
-
const
|
|
923
|
-
const
|
|
1272
|
+
const visibleNodes = this._tableTree.visibleNodes();
|
|
1273
|
+
const rowsById = this.rowsById();
|
|
1274
|
+
const rangeCount = scrollRange.end - scrollRange.start;
|
|
1275
|
+
const rowCount = Math.min(rangeCount, this._tableTree.loading() ? rangeCount : visibleNodes.length);
|
|
924
1276
|
// Populate rows with cached rows in the range window, fallback to row shell to show skeleton.
|
|
925
|
-
return untracked(() => Array.from({ length: rowCount }, (_, i) =>
|
|
1277
|
+
return untracked(() => Array.from({ length: rowCount }, (_, i) => {
|
|
1278
|
+
const node = visibleNodes[scrollRange.start + i];
|
|
1279
|
+
if (node && rowsById.has(node.id)) {
|
|
1280
|
+
return rowsById.get(node.id);
|
|
1281
|
+
}
|
|
1282
|
+
return createSkeletonRow(i);
|
|
1283
|
+
}));
|
|
926
1284
|
});
|
|
927
|
-
function
|
|
1285
|
+
function createSkeletonRow(index) {
|
|
928
1286
|
return {
|
|
929
1287
|
index,
|
|
930
|
-
|
|
1288
|
+
id: skeletonRowId,
|
|
1289
|
+
item: undefined,
|
|
1290
|
+
cells: [],
|
|
1291
|
+
bindings: coerceTableRowBindings([], undefined, index),
|
|
1292
|
+
level: 0,
|
|
1293
|
+
hasChildren: signal(false).asReadonly(),
|
|
1294
|
+
expanded: signal(false).asReadonly(),
|
|
931
1295
|
active: signal(false).asReadonly(),
|
|
932
1296
|
selected: signal(false).asReadonly(),
|
|
933
1297
|
hovered: signal(false).asReadonly(),
|
|
@@ -949,7 +1313,8 @@ class ɵSciTable {
|
|
|
949
1313
|
const start = Math.floor(scrollTop / itemHeight);
|
|
950
1314
|
const viewportRowCount = Math.ceil(viewportHeight / itemHeight);
|
|
951
1315
|
const end = Math.min(start + viewportRowCount);
|
|
952
|
-
|
|
1316
|
+
// fill viewport if no data loaded yet
|
|
1317
|
+
const totalCount = this._tableTree.loading() ? viewportRowCount : this.visibleRowCount();
|
|
953
1318
|
return {
|
|
954
1319
|
start: clamp(start - this.bufferSize, { min: 0, max: Math.max(0, totalCount - viewportRowCount) }),
|
|
955
1320
|
end: clamp(end + this.bufferSize, { max: totalCount }),
|
|
@@ -958,9 +1323,13 @@ class ɵSciTable {
|
|
|
958
1323
|
}
|
|
959
1324
|
computeVirtualScrollOffset() {
|
|
960
1325
|
return computed(() => {
|
|
1326
|
+
// Remove virtual-scroll padding so the error message and retry button are in view.
|
|
1327
|
+
if (this.error()) {
|
|
1328
|
+
return { top: 0, bottom: 0 };
|
|
1329
|
+
}
|
|
961
1330
|
const itemHeight = this.tableViewRef()?.itemHeight() ?? 0;
|
|
962
|
-
const totalCount = this.
|
|
963
|
-
const rangeEnd = Math.min(this.scrollRange()?.end ?? 0,
|
|
1331
|
+
const totalCount = this.visibleRowCount();
|
|
1332
|
+
const rangeEnd = Math.min(this.scrollRange()?.end ?? 0, totalCount);
|
|
964
1333
|
return {
|
|
965
1334
|
top: (this.scrollRange()?.start ?? 0) * itemHeight,
|
|
966
1335
|
bottom: (totalCount - rangeEnd) * itemHeight,
|
|
@@ -994,79 +1363,48 @@ class ɵSciTable {
|
|
|
994
1363
|
return rxResource({
|
|
995
1364
|
defaultValue: false,
|
|
996
1365
|
params: () => this.tableViewRef()?.viewport,
|
|
997
|
-
stream: ({ params: viewport }) => fromEvent(viewport, 'scroll', { passive: true })
|
|
998
|
-
.pipe(switchMap(() => concat(of(true), timer(150).pipe(map(() => false)))), startWith(false), subscribeIn(fn => zone.runOutsideAngular(fn))),
|
|
999
|
-
}).value;
|
|
1000
|
-
}
|
|
1001
|
-
/**
|
|
1002
|
-
* Loads a range of rows, based on the current sort and filter criteria, into the cache.
|
|
1003
|
-
*/
|
|
1004
|
-
async loadRange(start, end) {
|
|
1005
|
-
const sortCriteria = this.sortCriteria();
|
|
1006
|
-
const columnFilters = this.filterCriteria();
|
|
1007
|
-
const tableFilter = this._tableFilter() ?? undefined;
|
|
1008
|
-
// Calculate pages based on row indices and page size.
|
|
1009
|
-
const pages = pagesByRange(start, end, this.pageSize);
|
|
1010
|
-
if (!pages) {
|
|
1011
|
-
return;
|
|
1012
|
-
}
|
|
1013
|
-
// Load pages and wait until completed loading.
|
|
1014
|
-
await Promise.all(pages
|
|
1015
|
-
.map(page => this.loadPage({ page, pageSize: this.pageSize, columnFilters, tableFilter, sortCriteria }))
|
|
1016
|
-
.map(page => firstValueFrom(toLazyObservable(page.loading, { injector: this._injector }).pipe(first(loading => !loading), defaultIfEmpty(true)))));
|
|
1366
|
+
stream: ({ params: viewport }) => fromEvent(viewport, 'scroll', { passive: true })
|
|
1367
|
+
.pipe(switchMap(() => concat(of(true), timer(150).pipe(map(() => false)))), startWith(false), subscribeIn(fn => zone.runOutsideAngular(fn))),
|
|
1368
|
+
}).value;
|
|
1017
1369
|
}
|
|
1018
1370
|
/**
|
|
1019
1371
|
* Loads the requested page from the cache or datasource and returns its loading state with a cancelation handler.
|
|
1020
1372
|
*/
|
|
1021
|
-
loadPage(
|
|
1022
|
-
const
|
|
1023
|
-
|
|
1024
|
-
const cacheKey = `${pageStart}-${pageEnd}`;
|
|
1025
|
-
if (this._cache.has(cacheKey)) {
|
|
1373
|
+
loadPage(nodeIds) {
|
|
1374
|
+
const nodeIdsToLoad = nodeIds.filter(id => !this._cache.has(id));
|
|
1375
|
+
if (nodeIdsToLoad.length === 0) {
|
|
1026
1376
|
return {
|
|
1027
|
-
|
|
1028
|
-
cancel: () => this._cache.deleteIfLoading(cacheKey),
|
|
1377
|
+
cancel: () => this._cache.deleteIfLoading(nodeIds),
|
|
1029
1378
|
};
|
|
1030
1379
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
// Create
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
},
|
|
1058
|
-
}), map(({ rows }) => rows)),
|
|
1059
|
-
defaultValue: [],
|
|
1060
|
-
});
|
|
1061
|
-
}),
|
|
1062
|
-
dispose: () => cacheEntryInjector.destroy(),
|
|
1063
|
-
start: pageStart,
|
|
1064
|
-
end: pageEnd,
|
|
1065
|
-
};
|
|
1066
|
-
this._cache.set(cacheKey, cacheEntry);
|
|
1380
|
+
// Track consistent snapshot of columns and node Ids.
|
|
1381
|
+
// If the access happens directly in `mapItemsToRow` they can be out of sync because `nodesById` depends on `columns`.
|
|
1382
|
+
// Don't use the rowMapping data as resource params, since it should not refetch from the datasource on change.
|
|
1383
|
+
const snapshot = computed(() => {
|
|
1384
|
+
const columns = this.columns();
|
|
1385
|
+
const nodesById = this._tableTree.nodesById();
|
|
1386
|
+
const nodeIndexById = this._tableTree.nodeIndexById();
|
|
1387
|
+
return { columns, nodesById, nodeIndexById };
|
|
1388
|
+
}, /* @ts-ignore */
|
|
1389
|
+
...(ngDevMode ? [{ debugName: "snapshot" }] : /* istanbul ignore next */ []));
|
|
1390
|
+
// Create a resource to track loading and error states.
|
|
1391
|
+
this._cache.set(nodeIdsToLoad, rxResource({
|
|
1392
|
+
stream: () => {
|
|
1393
|
+
// Injector for the datasource call; destroyed on stream completion to clean up allocated resources.
|
|
1394
|
+
const datasourceInjector = _createDestroyableInjector({ parent: this._injector });
|
|
1395
|
+
// Create separate injection context per items emission to clean up allocated resources.
|
|
1396
|
+
const itemsInjectorFactory = new _SingleActiveInjectorFactory({ parent: datasourceInjector });
|
|
1397
|
+
return runInInjectionContext(datasourceInjector, () => Observables.coerce(this._datasource.getItems(nodeIdsToLoad)))
|
|
1398
|
+
.pipe(combineLatestWith(toLazyObservable(snapshot, { injector: datasourceInjector })), map(([response, { columns, nodesById, nodeIndexById }]) => runInInjectionContext(itemsInjectorFactory.create(), () => this.mapItemsToRow(response, columns, nodesById, nodeIndexById))), catchError(error => {
|
|
1399
|
+
console.error(error);
|
|
1400
|
+
throw error;
|
|
1401
|
+
}), finalize(() => datasourceInjector.destroy()));
|
|
1402
|
+
},
|
|
1403
|
+
injector: this._injector,
|
|
1404
|
+
defaultValue: [],
|
|
1405
|
+
}));
|
|
1067
1406
|
return {
|
|
1068
|
-
|
|
1069
|
-
cancel: () => this._cache.deleteIfLoading(cacheKey),
|
|
1407
|
+
cancel: () => this._cache.deleteIfLoading(nodeIdsToLoad),
|
|
1070
1408
|
};
|
|
1071
1409
|
}
|
|
1072
1410
|
/**
|
|
@@ -1105,25 +1443,30 @@ class ɵSciTable {
|
|
|
1105
1443
|
if (!column) {
|
|
1106
1444
|
throw Error(`[NullColumnError] Column '${options.columnName}' not found in table '${this.name()}'.`);
|
|
1107
1445
|
}
|
|
1108
|
-
this.
|
|
1446
|
+
this.columnFilters.update(filterCriteria => {
|
|
1109
1447
|
const newFilterCriteria = filterCriteria.filter(filterCriterion => filterCriterion.columnName !== options.columnName);
|
|
1110
1448
|
return text === null ? newFilterCriteria : newFilterCriteria.concat({ columnName: column.name, text });
|
|
1111
1449
|
});
|
|
1112
1450
|
}
|
|
1451
|
+
setActiveItem(id) {
|
|
1452
|
+
this._tableTree.setActiveNode(id);
|
|
1453
|
+
}
|
|
1113
1454
|
updateSelectedItems(updateFn) {
|
|
1114
|
-
this.
|
|
1455
|
+
this._tableTree.updateSelectedNodes(updateFn);
|
|
1115
1456
|
}
|
|
1116
1457
|
/**
|
|
1117
|
-
* Indicates whether the built-in
|
|
1118
|
-
*
|
|
1119
|
-
* Supports out-of-the-box filtering and sorting for built-in table columns.
|
|
1458
|
+
* Indicates whether the built-in in-memory datasource is used.
|
|
1120
1459
|
*/
|
|
1121
|
-
|
|
1122
|
-
return
|
|
1460
|
+
isInMemoryDatasource() {
|
|
1461
|
+
return this._datasource instanceof SciInMemoryTableDatasource;
|
|
1123
1462
|
}
|
|
1124
|
-
|
|
1463
|
+
reload() {
|
|
1125
1464
|
this._cache.clear();
|
|
1126
|
-
this.
|
|
1465
|
+
this._tableTree.reload();
|
|
1466
|
+
}
|
|
1467
|
+
reloadFailed() {
|
|
1468
|
+
this._cache.reloadFailed();
|
|
1469
|
+
this._tableTree.reloadFailed();
|
|
1127
1470
|
}
|
|
1128
1471
|
dispose() {
|
|
1129
1472
|
this._cache.clear();
|
|
@@ -1135,51 +1478,45 @@ class ɵSciTable {
|
|
|
1135
1478
|
installPageLoader() {
|
|
1136
1479
|
effect(onCleanup => {
|
|
1137
1480
|
const scrollRange = this.scrollRange();
|
|
1138
|
-
const
|
|
1139
|
-
const columnFilters = this.filterCriteria();
|
|
1140
|
-
const tableFilter = this._tableFilter() ?? undefined;
|
|
1481
|
+
const visibleNodes = this._tableTree.visibleNodes();
|
|
1141
1482
|
if (!scrollRange) {
|
|
1142
1483
|
return;
|
|
1143
1484
|
}
|
|
1144
1485
|
untracked(() => {
|
|
1145
|
-
//
|
|
1146
|
-
|
|
1147
|
-
const
|
|
1148
|
-
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
sortCriteria,
|
|
1153
|
-
tableFilter,
|
|
1154
|
-
columnFilters,
|
|
1155
|
-
});
|
|
1486
|
+
// Calculate start and end pages that contain the range.
|
|
1487
|
+
const startPage = Math.floor(scrollRange.start / this.pageSize);
|
|
1488
|
+
const endPage = Math.floor((scrollRange.end - 1) / this.pageSize);
|
|
1489
|
+
for (let page = startPage; page <= endPage; page++) {
|
|
1490
|
+
const index = page * this.pageSize;
|
|
1491
|
+
const nodes = visibleNodes.slice(index, index + this.pageSize).map(node => node.id);
|
|
1492
|
+
const pageRef = this.loadPage(nodes);
|
|
1156
1493
|
onCleanup(() => pageRef.cancel());
|
|
1157
|
-
}
|
|
1494
|
+
}
|
|
1158
1495
|
});
|
|
1159
1496
|
});
|
|
1160
1497
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
toObservable(this.criteria).pipe(skip(1), // skip first emission to avoid race condition with loader on initialization.
|
|
1166
|
-
takeUntilDestroyed()).subscribe(() => {
|
|
1167
|
-
this._cache.clear();
|
|
1168
|
-
});
|
|
1169
|
-
}
|
|
1170
|
-
mapItemsToRow(items, columns, pageStart) {
|
|
1171
|
-
return items.map((item, i) => {
|
|
1498
|
+
mapItemsToRow(items, columns, nodesById, nodeIndexById) {
|
|
1499
|
+
// Indicates whether there are nodes with children.
|
|
1500
|
+
const isTree = Array.from(nodesById.values()).some(node => node.parentId !== undefined);
|
|
1501
|
+
return items.map((item) => {
|
|
1172
1502
|
const id = this.trackBy(item);
|
|
1173
|
-
const index =
|
|
1503
|
+
const index = nodeIndexById.get(id);
|
|
1504
|
+
const node = nodesById.get(id);
|
|
1505
|
+
if (node === undefined || index === undefined) {
|
|
1506
|
+
return undefined;
|
|
1507
|
+
}
|
|
1174
1508
|
return {
|
|
1175
1509
|
id,
|
|
1176
1510
|
index,
|
|
1177
1511
|
item,
|
|
1178
|
-
|
|
1179
|
-
active: computed(() =>
|
|
1180
|
-
selected: computed(() => this.
|
|
1181
|
-
|
|
1182
|
-
|
|
1512
|
+
hovered: computed(() => this.hoveredIndex() === index),
|
|
1513
|
+
active: computed(() => this.activeItem() === id),
|
|
1514
|
+
selected: computed(() => this._tableTree.selectedNodes().has(id)),
|
|
1515
|
+
expanded: computed(() => this._tableTree.expandedNodes().has(id)),
|
|
1516
|
+
level: isTree ? calculateLevel(node) : undefined,
|
|
1517
|
+
parentId: node.parentId,
|
|
1518
|
+
hasChildren: computed(() => (this._tableTree.childrenIdsByParent().get(id)?.length ?? 0) > 0),
|
|
1519
|
+
bindings: coerceTableRowBindings(this._rowBindings, item, index),
|
|
1183
1520
|
cells: columns.map(column => ({
|
|
1184
1521
|
value: 'value' in column ? column.value(item) : undefined,
|
|
1185
1522
|
component: column.type === 'component' ? column.component(item) : undefined,
|
|
@@ -1188,7 +1525,13 @@ class ɵSciTable {
|
|
|
1188
1525
|
column,
|
|
1189
1526
|
})),
|
|
1190
1527
|
};
|
|
1191
|
-
});
|
|
1528
|
+
}).filter((node) => !!node);
|
|
1529
|
+
function calculateLevel(node) {
|
|
1530
|
+
if (node.parentId === undefined) {
|
|
1531
|
+
return 0;
|
|
1532
|
+
}
|
|
1533
|
+
return calculateLevel(nodesById.get(node.parentId)) + 1;
|
|
1534
|
+
}
|
|
1192
1535
|
}
|
|
1193
1536
|
/**
|
|
1194
1537
|
* Creates a signal to read and write table user settings.
|
|
@@ -1198,7 +1541,7 @@ class ɵSciTable {
|
|
|
1198
1541
|
const userSettings = resource({ ...(ngDevMode ? { debugName: "userSettings" } : /* istanbul ignore next */ {}), params: () => this.name(),
|
|
1199
1542
|
loader: async ({ params: tableName }) => {
|
|
1200
1543
|
try {
|
|
1201
|
-
const serialized = await this._tableStorage.load(`scion.components
|
|
1544
|
+
const serialized = await this._tableStorage.load(`scion.components:${tableName}`);
|
|
1202
1545
|
return serialized ? JSON.parse(serialized) : { columns: [] };
|
|
1203
1546
|
}
|
|
1204
1547
|
catch (error) {
|
|
@@ -1210,7 +1553,7 @@ class ɵSciTable {
|
|
|
1210
1553
|
// Persist settings to storage.
|
|
1211
1554
|
effect(() => {
|
|
1212
1555
|
if (userSettings.status() === 'local' && this.name()) {
|
|
1213
|
-
untracked(() => void this._tableStorage.store(`scion.components
|
|
1556
|
+
untracked(() => void this._tableStorage.store(`scion.components:${this.name()}`, JSON.stringify(userSettings.value())));
|
|
1214
1557
|
}
|
|
1215
1558
|
});
|
|
1216
1559
|
return userSettings.value;
|
|
@@ -1219,30 +1562,39 @@ class ɵSciTable {
|
|
|
1219
1562
|
* Call trackBy function or fallback to track by object reference.
|
|
1220
1563
|
*/
|
|
1221
1564
|
trackBy(item) {
|
|
1222
|
-
|
|
1565
|
+
if (this._trackBy !== undefined) {
|
|
1566
|
+
return this._trackBy(item);
|
|
1567
|
+
}
|
|
1568
|
+
return item;
|
|
1223
1569
|
}
|
|
1224
1570
|
computeActiveRow() {
|
|
1225
1571
|
return computed(() => {
|
|
1226
1572
|
const activeItem = this.activeItem();
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
}
|
|
1230
|
-
const id = this.trackBy(activeItem);
|
|
1231
|
-
if (id === undefined) {
|
|
1232
|
-
return undefined;
|
|
1233
|
-
}
|
|
1234
|
-
return this._cache.rowsById().get(id);
|
|
1573
|
+
const rowsById = this._cache.rowsById();
|
|
1574
|
+
return rowsById.has(activeItem) ? rowsById.get(activeItem) : undefined;
|
|
1235
1575
|
});
|
|
1236
1576
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1577
|
+
select(ids) {
|
|
1578
|
+
this._tableTree.select(ids);
|
|
1579
|
+
}
|
|
1580
|
+
expand(ids) {
|
|
1581
|
+
this._tableTree.expand(ids);
|
|
1582
|
+
}
|
|
1583
|
+
isExpanded(id) {
|
|
1584
|
+
return this._tableTree.expandedNodes().has(id);
|
|
1585
|
+
}
|
|
1586
|
+
expandAll() {
|
|
1587
|
+
this._tableTree.expandAll();
|
|
1588
|
+
}
|
|
1589
|
+
collapse(ids) {
|
|
1590
|
+
this._tableTree.collapse(ids);
|
|
1591
|
+
}
|
|
1592
|
+
collapseAll() {
|
|
1593
|
+
this._tableTree.collapseAll();
|
|
1594
|
+
}
|
|
1595
|
+
toggleRow(id) {
|
|
1596
|
+
this._tableTree.toggleRow(id);
|
|
1597
|
+
}
|
|
1246
1598
|
}
|
|
1247
1599
|
const ɵSCI_TABLE = new InjectionToken('ɵSciTable');
|
|
1248
1600
|
|
|
@@ -1255,16 +1607,16 @@ const ɵSCI_TABLE = new InjectionToken('ɵSciTable');
|
|
|
1255
1607
|
*
|
|
1256
1608
|
* SPDX-License-Identifier: EPL-2.0
|
|
1257
1609
|
*/
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1610
|
+
function table(dataOrDescriptor, columnFactoryFn, options) {
|
|
1611
|
+
assertNotInReactiveContext(table, 'Call table in a non-reactive (non-tracking) context, such as within the untracked() function.');
|
|
1612
|
+
const descriptor = isSignal(dataOrDescriptor) ? ({ datasource: dataOrDescriptor, columns: columnFactoryFn, injector: options?.injector }) : dataOrDescriptor;
|
|
1613
|
+
if (!descriptor.injector) {
|
|
1614
|
+
assertInInjectionContext(table);
|
|
1615
|
+
}
|
|
1616
|
+
const injector = descriptor.injector ?? inject(Injector);
|
|
1617
|
+
const sciTable = runInInjectionContext(injector, () => new ɵSciTable(descriptor));
|
|
1618
|
+
injector.get(DestroyRef).onDestroy(() => sciTable.dispose());
|
|
1619
|
+
return sciTable;
|
|
1268
1620
|
}
|
|
1269
1621
|
|
|
1270
1622
|
/*
|
|
@@ -1276,16 +1628,33 @@ function ɵillegaldatasource() {
|
|
|
1276
1628
|
*
|
|
1277
1629
|
* SPDX-License-Identifier: EPL-2.0
|
|
1278
1630
|
*/
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1631
|
+
/**
|
|
1632
|
+
* Creates a range of integers starting at `start` and ending at `end` (including `end`).
|
|
1633
|
+
*/
|
|
1634
|
+
function rangeInclusive(start, end) {
|
|
1635
|
+
const range = [];
|
|
1636
|
+
for (let i = start; i <= end; i++) {
|
|
1637
|
+
range.push(i);
|
|
1284
1638
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1639
|
+
return range;
|
|
1640
|
+
}
|
|
1641
|
+
/**
|
|
1642
|
+
* Indicates whether the given event was triggered from an input element.
|
|
1643
|
+
*/
|
|
1644
|
+
function isFromInput(event) {
|
|
1645
|
+
if (event.target instanceof Element && event.target.closest('input, textarea, select') !== null) {
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
if (event.target instanceof HTMLElement && event.target.isContentEditable) {
|
|
1649
|
+
return true;
|
|
1650
|
+
}
|
|
1651
|
+
return false;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* Indicates whether the given keyboard event was triggered from a button.
|
|
1655
|
+
*/
|
|
1656
|
+
function isFromButton(event) {
|
|
1657
|
+
return event.target instanceof Element && event.target.closest('button') !== null;
|
|
1289
1658
|
}
|
|
1290
1659
|
|
|
1291
1660
|
/*
|
|
@@ -1301,218 +1670,114 @@ class SciTableSelectionService {
|
|
|
1301
1670
|
_table = inject(ɵSCI_TABLE);
|
|
1302
1671
|
async onRowClick(index, event) {
|
|
1303
1672
|
const table = this._table();
|
|
1304
|
-
const
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
table.activeItem.set(item);
|
|
1309
|
-
if (item === undefined || !table.selectable()) {
|
|
1673
|
+
const rowId = table.visibleRows()?.[index]?.id;
|
|
1674
|
+
const previousActiveIndex = table.activeIndex();
|
|
1675
|
+
table.setActiveItem(rowId);
|
|
1676
|
+
if (rowId === undefined || !table.selectable()) {
|
|
1310
1677
|
return;
|
|
1311
1678
|
}
|
|
1312
1679
|
if (table.selectable() === 'single') {
|
|
1313
|
-
table.updateSelectedItems(() => new
|
|
1680
|
+
table.updateSelectedItems(() => new Set().add(rowId));
|
|
1314
1681
|
return;
|
|
1315
1682
|
}
|
|
1316
|
-
if (event.shiftKey &&
|
|
1317
|
-
const start = Math.min(
|
|
1318
|
-
const end = Math.max(
|
|
1683
|
+
if (event.shiftKey && previousActiveIndex >= 0) {
|
|
1684
|
+
const start = Math.min(previousActiveIndex, index);
|
|
1685
|
+
const end = Math.max(previousActiveIndex, index);
|
|
1319
1686
|
await this.selectItems(start, end, { add: true });
|
|
1320
1687
|
}
|
|
1321
1688
|
else if (event.ctrlKey || event.metaKey) {
|
|
1322
|
-
this.toggleSelectedItem(
|
|
1689
|
+
this.toggleSelectedItem(rowId);
|
|
1323
1690
|
}
|
|
1324
1691
|
else {
|
|
1325
1692
|
// If no modifier is pressed set the selection to the clicked row.
|
|
1326
|
-
table.updateSelectedItems(() => new
|
|
1693
|
+
table.updateSelectedItems(() => new Set().add(rowId));
|
|
1327
1694
|
}
|
|
1328
1695
|
}
|
|
1329
1696
|
async onArrowUp(event) {
|
|
1330
1697
|
event.preventDefault();
|
|
1331
1698
|
const table = this._table();
|
|
1332
|
-
const activeIndex =
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
}
|
|
1336
|
-
// Set active item.
|
|
1337
|
-
const startIndex = activeIndex - 1;
|
|
1338
|
-
await this.loadMissingItems(startIndex, startIndex);
|
|
1339
|
-
const startItem = table.rowsByIndex().get(startIndex)?.item;
|
|
1340
|
-
table.activeItem.set(startItem);
|
|
1341
|
-
if (!table.selectable()) {
|
|
1342
|
-
return;
|
|
1343
|
-
}
|
|
1344
|
-
const keyboardEvent = event;
|
|
1345
|
-
const shift = keyboardEvent.shiftKey;
|
|
1346
|
-
const ctrlOrMeta = keyboardEvent.ctrlKey || keyboardEvent.metaKey;
|
|
1347
|
-
if (shift && ctrlOrMeta) {
|
|
1348
|
-
await this.ctrlShiftUp({ startIndex, activeIndex });
|
|
1349
|
-
}
|
|
1350
|
-
else if (shift) {
|
|
1351
|
-
await this.shiftUp({ startIndex, activeIndex });
|
|
1352
|
-
}
|
|
1353
|
-
else if (ctrlOrMeta) {
|
|
1354
|
-
// Selection remains unchanged.
|
|
1355
|
-
}
|
|
1356
|
-
else {
|
|
1357
|
-
await this.selectItems(startIndex, startIndex);
|
|
1358
|
-
}
|
|
1699
|
+
const activeIndex = table.activeIndex();
|
|
1700
|
+
const targetIndex = Math.max(activeIndex - 1, 0);
|
|
1701
|
+
await this.selectOnKeyboardNavigation(targetIndex, event);
|
|
1359
1702
|
}
|
|
1360
1703
|
async onArrowDown(event) {
|
|
1361
1704
|
event.preventDefault();
|
|
1362
1705
|
const table = this._table();
|
|
1363
|
-
const activeIndex =
|
|
1364
|
-
const
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
if (!table.selectable()) {
|
|
1706
|
+
const activeIndex = table.activeIndex();
|
|
1707
|
+
const targetIndex = Math.min(activeIndex + 1, table.visibleRowCount() - 1);
|
|
1708
|
+
await this.selectOnKeyboardNavigation(targetIndex, event);
|
|
1709
|
+
}
|
|
1710
|
+
async onArrowLeft(event) {
|
|
1711
|
+
event.preventDefault();
|
|
1712
|
+
const table = this._table();
|
|
1713
|
+
const activeRow = table.activeRow();
|
|
1714
|
+
if (activeRow?.item === undefined || activeRow.id === skeletonRowId) {
|
|
1373
1715
|
return;
|
|
1374
1716
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
const ctrlOrMeta = keyboardEvent.ctrlKey || keyboardEvent.metaKey;
|
|
1378
|
-
if (shift && ctrlOrMeta) {
|
|
1379
|
-
await this.ctrlShiftDown({ endIndex, activeIndex });
|
|
1380
|
-
}
|
|
1381
|
-
else if (shift) {
|
|
1382
|
-
await this.shiftDown({ endIndex, activeIndex });
|
|
1717
|
+
if (activeRow.hasChildren() && activeRow.expanded()) {
|
|
1718
|
+
table.collapse(activeRow.id);
|
|
1383
1719
|
}
|
|
1384
|
-
else if (
|
|
1385
|
-
|
|
1720
|
+
else if (activeRow.parentId !== undefined) {
|
|
1721
|
+
const parentIndex = table.rowIndexById().get(activeRow.parentId) ?? -1;
|
|
1722
|
+
await this.selectOnKeyboardNavigation(parentIndex, { shiftKey: false, ctrlKey: false, metaKey: false });
|
|
1386
1723
|
}
|
|
1724
|
+
// Root case
|
|
1387
1725
|
else {
|
|
1388
|
-
|
|
1726
|
+
const rows = table.visibleRows();
|
|
1727
|
+
const row = findPreviousExpandedRow(activeRow.index - 1);
|
|
1728
|
+
if (row !== undefined) {
|
|
1729
|
+
const index = table.rowIndexById().get(row.id) ?? -1;
|
|
1730
|
+
await this.selectOnKeyboardNavigation(index, { shiftKey: false, ctrlKey: false, metaKey: false });
|
|
1731
|
+
}
|
|
1732
|
+
function findPreviousExpandedRow(index) {
|
|
1733
|
+
while (index > 0) {
|
|
1734
|
+
const row = rows?.[index];
|
|
1735
|
+
// Get the row expansion state whether the row is already loaded or not.
|
|
1736
|
+
if (row !== undefined && table.isExpanded(row.id)) {
|
|
1737
|
+
return row;
|
|
1738
|
+
}
|
|
1739
|
+
index--;
|
|
1740
|
+
}
|
|
1741
|
+
// Return the first row if no previous row is expanded.
|
|
1742
|
+
return rows?.[index];
|
|
1743
|
+
}
|
|
1389
1744
|
}
|
|
1390
1745
|
}
|
|
1391
|
-
async
|
|
1746
|
+
async onArrowRight(event) {
|
|
1392
1747
|
event.preventDefault();
|
|
1393
1748
|
const table = this._table();
|
|
1394
|
-
const
|
|
1395
|
-
|
|
1749
|
+
const activeRow = table.activeRow();
|
|
1750
|
+
const activeIndex = table.activeIndex();
|
|
1751
|
+
const endIndex = Math.min(activeIndex + 1, table.visibleRowCount() - 1);
|
|
1752
|
+
if (activeRow?.id !== skeletonRowId && activeRow?.hasChildren() && !activeRow.expanded()) {
|
|
1753
|
+
table.expand(activeRow.id);
|
|
1396
1754
|
return;
|
|
1397
1755
|
}
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
table.
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
const keyboardEvent = event;
|
|
1407
|
-
const shift = keyboardEvent.shiftKey;
|
|
1408
|
-
const ctrlOrMeta = keyboardEvent.ctrlKey || keyboardEvent.metaKey;
|
|
1409
|
-
if (shift && ctrlOrMeta) {
|
|
1410
|
-
await this.ctrlShiftUp({ startIndex, activeIndex });
|
|
1411
|
-
}
|
|
1412
|
-
else if (shift) {
|
|
1413
|
-
await this.shiftUp({ startIndex, activeIndex });
|
|
1414
|
-
}
|
|
1415
|
-
else if (ctrlOrMeta) {
|
|
1416
|
-
// Selection remains unchanged.
|
|
1417
|
-
}
|
|
1418
|
-
else {
|
|
1419
|
-
await this.selectItems(startIndex, startIndex);
|
|
1420
|
-
}
|
|
1756
|
+
await this.selectOnKeyboardNavigation(endIndex, { shiftKey: false, ctrlKey: false, metaKey: false });
|
|
1757
|
+
}
|
|
1758
|
+
async onPageUp(event) {
|
|
1759
|
+
event.preventDefault();
|
|
1760
|
+
const table = this._table();
|
|
1761
|
+
const activeIndex = table.activeIndex();
|
|
1762
|
+
const targetIndex = Math.max(activeIndex - table.viewportPageSize() + 1, 0);
|
|
1763
|
+
await this.selectOnKeyboardNavigation(targetIndex, event);
|
|
1421
1764
|
}
|
|
1422
1765
|
async onPageDown(event) {
|
|
1423
1766
|
event.preventDefault();
|
|
1424
1767
|
const table = this._table();
|
|
1425
|
-
const activeIndex =
|
|
1426
|
-
const
|
|
1427
|
-
|
|
1428
|
-
return;
|
|
1429
|
-
}
|
|
1430
|
-
// Set active item.
|
|
1431
|
-
await this.loadMissingItems(endIndex, endIndex);
|
|
1432
|
-
const endItem = table.rowsByIndex().get(endIndex)?.item;
|
|
1433
|
-
table.activeItem.set(endItem);
|
|
1434
|
-
if (!table.selectable()) {
|
|
1435
|
-
return;
|
|
1436
|
-
}
|
|
1437
|
-
const keyboardEvent = event;
|
|
1438
|
-
const shift = keyboardEvent.shiftKey;
|
|
1439
|
-
const ctrlOrMeta = keyboardEvent.ctrlKey || keyboardEvent.metaKey;
|
|
1440
|
-
if (shift && ctrlOrMeta) {
|
|
1441
|
-
await this.ctrlShiftDown({ endIndex, activeIndex });
|
|
1442
|
-
}
|
|
1443
|
-
else if (shift) {
|
|
1444
|
-
await this.shiftDown({ endIndex, activeIndex });
|
|
1445
|
-
}
|
|
1446
|
-
else if (ctrlOrMeta) {
|
|
1447
|
-
// Selection remains unchanged.
|
|
1448
|
-
}
|
|
1449
|
-
else {
|
|
1450
|
-
await this.selectItems(endIndex, endIndex);
|
|
1451
|
-
}
|
|
1768
|
+
const activeIndex = table.activeIndex();
|
|
1769
|
+
const targetIndex = Math.min(activeIndex + table.viewportPageSize() - 1, table.visibleRowCount() - 1);
|
|
1770
|
+
await this.selectOnKeyboardNavigation(targetIndex, event);
|
|
1452
1771
|
}
|
|
1453
1772
|
async onHome(event) {
|
|
1454
1773
|
event.preventDefault();
|
|
1455
|
-
|
|
1456
|
-
const activeIndex = this._table().activeRow()?.index ?? -1;
|
|
1457
|
-
if (activeIndex <= 0) {
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
// Set active item.
|
|
1461
|
-
const startIndex = 0;
|
|
1462
|
-
await this.loadMissingItems(startIndex, startIndex);
|
|
1463
|
-
const startItem = table.rowsByIndex().get(startIndex)?.item;
|
|
1464
|
-
table.activeItem.set(startItem);
|
|
1465
|
-
if (!table.selectable()) {
|
|
1466
|
-
return;
|
|
1467
|
-
}
|
|
1468
|
-
const keyboardEvent = event;
|
|
1469
|
-
const shift = keyboardEvent.shiftKey;
|
|
1470
|
-
const ctrlOrMeta = keyboardEvent.ctrlKey || keyboardEvent.metaKey;
|
|
1471
|
-
if (shift && ctrlOrMeta) {
|
|
1472
|
-
await this.ctrlShiftUp({ startIndex, activeIndex });
|
|
1473
|
-
}
|
|
1474
|
-
else if (shift) {
|
|
1475
|
-
await this.shiftUp({ startIndex, activeIndex });
|
|
1476
|
-
}
|
|
1477
|
-
else if (ctrlOrMeta) {
|
|
1478
|
-
// Selection remains unchanged.
|
|
1479
|
-
}
|
|
1480
|
-
else {
|
|
1481
|
-
await this.selectItems(startIndex, startIndex);
|
|
1482
|
-
}
|
|
1774
|
+
await this.selectOnKeyboardNavigation(0, event);
|
|
1483
1775
|
}
|
|
1484
1776
|
async onEnd(event) {
|
|
1485
1777
|
event.preventDefault();
|
|
1486
1778
|
const table = this._table();
|
|
1487
|
-
const
|
|
1488
|
-
|
|
1489
|
-
if (endIndex === activeIndex) {
|
|
1490
|
-
return;
|
|
1491
|
-
}
|
|
1492
|
-
// TODO [mmu] should active item be set before selection? this way, further methods cant rely on table.activeItem.
|
|
1493
|
-
// But otherwise, loading and setting the active item needs to be done for each case, or once at the end.
|
|
1494
|
-
// Set active item.
|
|
1495
|
-
await this.loadMissingItems(endIndex, endIndex);
|
|
1496
|
-
const endItem = table.rowsByIndex().get(endIndex)?.item;
|
|
1497
|
-
table.activeItem.set(endItem);
|
|
1498
|
-
if (!table.selectable()) {
|
|
1499
|
-
return;
|
|
1500
|
-
}
|
|
1501
|
-
const keyboardEvent = event;
|
|
1502
|
-
const shift = keyboardEvent.shiftKey;
|
|
1503
|
-
const ctrlOrMeta = keyboardEvent.ctrlKey || keyboardEvent.metaKey;
|
|
1504
|
-
if (shift && ctrlOrMeta) {
|
|
1505
|
-
await this.ctrlShiftDown({ endIndex, activeIndex });
|
|
1506
|
-
}
|
|
1507
|
-
else if (shift) {
|
|
1508
|
-
await this.shiftDown({ endIndex, activeIndex });
|
|
1509
|
-
}
|
|
1510
|
-
else if (ctrlOrMeta) {
|
|
1511
|
-
// Selection remains unchanged.
|
|
1512
|
-
}
|
|
1513
|
-
else {
|
|
1514
|
-
await this.selectItems(endIndex, endIndex);
|
|
1515
|
-
}
|
|
1779
|
+
const targetIndex = table.visibleRowCount() - 1;
|
|
1780
|
+
await this.selectOnKeyboardNavigation(targetIndex, event);
|
|
1516
1781
|
}
|
|
1517
1782
|
onSpace(event) {
|
|
1518
1783
|
event.preventDefault();
|
|
@@ -1520,168 +1785,126 @@ class SciTableSelectionService {
|
|
|
1520
1785
|
if (!table.selectable()) {
|
|
1521
1786
|
return;
|
|
1522
1787
|
}
|
|
1523
|
-
const
|
|
1524
|
-
if (
|
|
1788
|
+
const activeItemId = table.activeItem();
|
|
1789
|
+
if (activeItemId === undefined) {
|
|
1525
1790
|
return;
|
|
1526
1791
|
}
|
|
1527
1792
|
const keyboardEvent = event;
|
|
1528
1793
|
const ctrlOrMeta = keyboardEvent.ctrlKey || keyboardEvent.metaKey;
|
|
1529
|
-
const activeItemId = table.trackBy(activeItem);
|
|
1530
1794
|
if (ctrlOrMeta) {
|
|
1531
|
-
this.toggleSelectedItem(
|
|
1795
|
+
this.toggleSelectedItem(activeItemId);
|
|
1532
1796
|
}
|
|
1533
1797
|
else {
|
|
1534
|
-
table.updateSelectedItems(() => new
|
|
1798
|
+
table.updateSelectedItems(() => new Set().add(activeItemId));
|
|
1535
1799
|
}
|
|
1536
1800
|
}
|
|
1537
1801
|
async onControlA(event) {
|
|
1538
1802
|
event.preventDefault();
|
|
1539
1803
|
const table = this._table();
|
|
1540
|
-
const totalCount = table.
|
|
1804
|
+
const totalCount = table.visibleRowCount();
|
|
1541
1805
|
if (table.selectable() === 'multi' && totalCount >= 0) {
|
|
1542
1806
|
await this.selectItems(0, totalCount - 1);
|
|
1543
1807
|
}
|
|
1544
1808
|
}
|
|
1545
|
-
|
|
1546
|
-
// shiftArrowDown
|
|
1547
|
-
// shiftPageDown
|
|
1548
|
-
// ShiftEnd
|
|
1549
|
-
async shiftDown(options) {
|
|
1809
|
+
async selectOnKeyboardNavigation(targetIndex, event) {
|
|
1550
1810
|
const table = this._table();
|
|
1551
|
-
const
|
|
1552
|
-
const activeIndex =
|
|
1553
|
-
const
|
|
1554
|
-
const endIndex =
|
|
1555
|
-
const
|
|
1556
|
-
const
|
|
1557
|
-
if (table.selectable()
|
|
1558
|
-
|
|
1811
|
+
const targetItem = table.visibleRows()?.[targetIndex]?.id;
|
|
1812
|
+
const activeIndex = table.activeIndex();
|
|
1813
|
+
const direction = activeIndex > targetIndex ? 'up' : 'down';
|
|
1814
|
+
const [startIndex, endIndex] = direction === 'up' ? [targetIndex, activeIndex] : [activeIndex, targetIndex];
|
|
1815
|
+
const shift = event.shiftKey;
|
|
1816
|
+
const ctrlOrMeta = event.ctrlKey || event.metaKey;
|
|
1817
|
+
if (table.selectable() !== 'multi') {
|
|
1818
|
+
// Don't modify selection when only ctrlOrMeta is pressed.
|
|
1819
|
+
if (table.selectable() === 'single' && (shift || !ctrlOrMeta)) {
|
|
1820
|
+
await this.selectItems(targetIndex, targetIndex);
|
|
1821
|
+
}
|
|
1822
|
+
table.setActiveItem(targetItem);
|
|
1559
1823
|
return;
|
|
1560
1824
|
}
|
|
1561
|
-
if (
|
|
1562
|
-
await this.selectItems(startIndex, endIndex);
|
|
1563
|
-
return;
|
|
1825
|
+
if (shift && ctrlOrMeta) {
|
|
1826
|
+
await this.selectItems(startIndex, endIndex, { add: true });
|
|
1564
1827
|
}
|
|
1565
|
-
if (
|
|
1566
|
-
await this.
|
|
1567
|
-
return;
|
|
1828
|
+
else if (shift) {
|
|
1829
|
+
await this.selectOnShiftNavigation(direction, startIndex, endIndex);
|
|
1568
1830
|
}
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
async ctrlShiftDown(options) {
|
|
1572
|
-
const table = this._table();
|
|
1573
|
-
const startIndex = options.activeIndex;
|
|
1574
|
-
const endIndex = options.endIndex;
|
|
1575
|
-
if (table.selectable() === 'single') {
|
|
1576
|
-
await this.selectItems(endIndex, endIndex);
|
|
1831
|
+
else if (ctrlOrMeta) {
|
|
1832
|
+
// Selection remains unchanged.
|
|
1577
1833
|
}
|
|
1578
1834
|
else {
|
|
1579
|
-
await this.selectItems(
|
|
1835
|
+
await this.selectItems(targetIndex, targetIndex);
|
|
1580
1836
|
}
|
|
1837
|
+
table.setActiveItem(targetItem);
|
|
1581
1838
|
}
|
|
1582
|
-
|
|
1839
|
+
selectOnShiftNavigation(direction, startIndex, endIndex) {
|
|
1583
1840
|
const table = this._table();
|
|
1584
|
-
const
|
|
1585
|
-
const
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
const activeItemId = table.trackBy(activeItem);
|
|
1590
|
-
if (table.selectable() === 'single') {
|
|
1591
|
-
await this.selectItems(startIndex, startIndex);
|
|
1592
|
-
return;
|
|
1593
|
-
}
|
|
1594
|
-
if (!selectedIds.has(activeItemId)) {
|
|
1595
|
-
await this.selectItems(startIndex, endIndex);
|
|
1596
|
-
return;
|
|
1841
|
+
const activeIndex = table.activeIndex();
|
|
1842
|
+
const selectedIds = table.selectedItems();
|
|
1843
|
+
// If the active item is outside the selection, start a new selection.
|
|
1844
|
+
if (!selectedIds.includes(table.activeItem())) {
|
|
1845
|
+
return this.selectItems(startIndex, endIndex);
|
|
1597
1846
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
await this.selectItems(startIndex, endIndex, { add: true });
|
|
1603
|
-
}
|
|
1604
|
-
async ctrlShiftUp(options) {
|
|
1605
|
-
const table = this._table();
|
|
1606
|
-
const startIndex = options.startIndex;
|
|
1607
|
-
const endIndex = options.activeIndex;
|
|
1608
|
-
if (table.selectable() === 'single') {
|
|
1609
|
-
await this.selectItems(startIndex, startIndex);
|
|
1847
|
+
// Moving down from the block start shrinks the selection until the block end, then extends it.
|
|
1848
|
+
if (direction === 'down' && this.isSelectionAtBlockStart(activeIndex)) {
|
|
1849
|
+
return this.selectFromBlockStart(startIndex, endIndex);
|
|
1610
1850
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1851
|
+
// Moving up from the block end shrinks the selection until the block start, then extends it.
|
|
1852
|
+
if (direction === 'up' && this.isSelectionAtBlockEnd(activeIndex)) {
|
|
1853
|
+
return this.selectFromBlockEnd(startIndex, endIndex);
|
|
1613
1854
|
}
|
|
1855
|
+
// In other cases, e.g. going down at the end of a selection block, select all items in the range.
|
|
1856
|
+
return this.selectItems(startIndex, endIndex, { add: true });
|
|
1614
1857
|
}
|
|
1615
1858
|
isSelectionAtBlockStart(activeIndex) {
|
|
1616
1859
|
const table = this._table();
|
|
1617
|
-
const selectedIds = table.
|
|
1618
|
-
const
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
const nextItemId = nextItem ? table.trackBy(nextItem) : undefined;
|
|
1622
|
-
return !selectedIds.has(previousItemId) && selectedIds.has(nextItemId);
|
|
1860
|
+
const selectedIds = table.selectedItems();
|
|
1861
|
+
const previousItemId = table.visibleRows()?.[activeIndex - 1]?.id;
|
|
1862
|
+
const nextItemId = table.visibleRows()?.[activeIndex + 1]?.id;
|
|
1863
|
+
return (previousItemId === undefined || !selectedIds.includes(previousItemId)) && nextItemId !== undefined && selectedIds.includes(nextItemId);
|
|
1623
1864
|
}
|
|
1624
1865
|
isSelectionAtBlockEnd(activeIndex) {
|
|
1625
1866
|
const table = this._table();
|
|
1626
|
-
const selectedIds = table.
|
|
1627
|
-
const
|
|
1628
|
-
const
|
|
1629
|
-
|
|
1630
|
-
const nextItemId = nextItem ? table.trackBy(nextItem) : undefined;
|
|
1631
|
-
return selectedIds.has(previousItemId) && !selectedIds.has(nextItemId);
|
|
1632
|
-
}
|
|
1633
|
-
async loadMissingItems(startIndex, endIndex) {
|
|
1634
|
-
const table = this._table();
|
|
1635
|
-
const indices = rangeInclusive(startIndex, endIndex + 1);
|
|
1636
|
-
const rows = indices.map(i => table.rowsByIndex().get(i));
|
|
1637
|
-
if (rows.some(row => row?.id === undefined)) {
|
|
1638
|
-
await Promise.race([
|
|
1639
|
-
table.loadRange(startIndex, endIndex + 1),
|
|
1640
|
-
firstValueFrom(timer(5_000)),
|
|
1641
|
-
]);
|
|
1642
|
-
}
|
|
1867
|
+
const selectedIds = table.selectedItems();
|
|
1868
|
+
const previousItemId = table.visibleRows()?.[activeIndex - 1]?.id;
|
|
1869
|
+
const nextItemId = table.visibleRows()?.[activeIndex + 1]?.id;
|
|
1870
|
+
return previousItemId !== undefined && selectedIds.includes(previousItemId) && (nextItemId === undefined || !selectedIds.includes(nextItemId));
|
|
1643
1871
|
}
|
|
1644
1872
|
async selectItems(startIndex, endIndex, options) {
|
|
1645
1873
|
const table = this._table();
|
|
1874
|
+
if (!table.selectable()) {
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1646
1877
|
const indices = rangeInclusive(startIndex, endIndex);
|
|
1647
1878
|
const add = options?.add ?? false;
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
// If not all id's could be found load the missing items.
|
|
1651
|
-
await Promise.race([
|
|
1652
|
-
table.loadRange(startIndex, endIndex),
|
|
1653
|
-
firstValueFrom(timer(5_000)),
|
|
1654
|
-
]);
|
|
1655
|
-
rows = indices.map(i => table.rowsByIndex().get(i));
|
|
1656
|
-
}
|
|
1657
|
-
table.updateSelectedItems(existing => new Map([
|
|
1879
|
+
const rows = indices.map(i => table.visibleRows()?.[i]);
|
|
1880
|
+
table.updateSelectedItems(existing => new Set([
|
|
1658
1881
|
...(add ? existing : []),
|
|
1659
1882
|
...rows
|
|
1660
|
-
.filter((row) => row
|
|
1661
|
-
.map(
|
|
1883
|
+
.filter((row) => !!row)
|
|
1884
|
+
.map(row => row.id),
|
|
1662
1885
|
]));
|
|
1663
1886
|
}
|
|
1664
1887
|
async selectFromBlockStart(startIndex, endIndex) {
|
|
1665
1888
|
const table = this._table();
|
|
1666
|
-
await this.loadMissingItems(startIndex, endIndex);
|
|
1667
1889
|
const indices = rangeInclusive(startIndex, endIndex);
|
|
1668
1890
|
table.updateSelectedItems(selection => {
|
|
1669
|
-
const newSelection = new
|
|
1891
|
+
const newSelection = new Set(selection);
|
|
1892
|
+
let shrinking = true;
|
|
1670
1893
|
for (let i = 0; i < indices.length; i++) {
|
|
1671
1894
|
const currentIndex = indices[i];
|
|
1672
1895
|
const nextIndex = indices[i + 1];
|
|
1673
|
-
const
|
|
1674
|
-
if (
|
|
1896
|
+
const itemId = table.visibleRows()?.[currentIndex]?.id;
|
|
1897
|
+
if (itemId === undefined) {
|
|
1675
1898
|
continue;
|
|
1676
1899
|
}
|
|
1677
|
-
const
|
|
1678
|
-
|
|
1679
|
-
const nextItemId = nextItem ? table.trackBy(nextItem) : undefined;
|
|
1680
|
-
if (selection.has(nextItemId)) {
|
|
1900
|
+
const nextItemId = table.visibleRows()?.[nextIndex]?.id;
|
|
1901
|
+
if (shrinking && nextItemId !== undefined && selection.has(nextItemId)) {
|
|
1681
1902
|
newSelection.delete(itemId);
|
|
1682
1903
|
}
|
|
1683
1904
|
else {
|
|
1684
|
-
|
|
1905
|
+
// Once outside the active block, only add items, even when crossing another selected block.
|
|
1906
|
+
shrinking = false;
|
|
1907
|
+
newSelection.add(itemId);
|
|
1685
1908
|
}
|
|
1686
1909
|
}
|
|
1687
1910
|
return newSelection;
|
|
@@ -1689,35 +1912,34 @@ class SciTableSelectionService {
|
|
|
1689
1912
|
}
|
|
1690
1913
|
async selectFromBlockEnd(startIndex, endIndex) {
|
|
1691
1914
|
const table = this._table();
|
|
1692
|
-
await this.loadMissingItems(startIndex, endIndex);
|
|
1693
1915
|
const indices = rangeInclusive(startIndex, endIndex);
|
|
1694
1916
|
table.updateSelectedItems(selection => {
|
|
1695
|
-
const newSelection = new
|
|
1917
|
+
const newSelection = new Set(selection);
|
|
1918
|
+
let shrinking = true;
|
|
1696
1919
|
for (let i = indices.length - 1; i >= 0; i--) {
|
|
1697
1920
|
const currentIndex = indices[i];
|
|
1698
1921
|
const previousIndex = indices[i - 1];
|
|
1699
|
-
const
|
|
1700
|
-
if (
|
|
1922
|
+
const itemId = table.visibleRows()?.[currentIndex]?.id;
|
|
1923
|
+
if (itemId === undefined) {
|
|
1701
1924
|
continue;
|
|
1702
1925
|
}
|
|
1703
|
-
const
|
|
1704
|
-
|
|
1705
|
-
const previousItemId = previousItem ? table.trackBy(previousItem) : undefined;
|
|
1706
|
-
if (selection.has(previousItemId)) {
|
|
1926
|
+
const previousItemId = table.visibleRows()?.[previousIndex]?.id;
|
|
1927
|
+
if (shrinking && previousItemId !== undefined && selection.has(previousItemId)) {
|
|
1707
1928
|
newSelection.delete(itemId);
|
|
1708
1929
|
}
|
|
1709
1930
|
else {
|
|
1710
|
-
|
|
1931
|
+
// Once outside the active block, only add items, even when crossing another selected block.
|
|
1932
|
+
shrinking = false;
|
|
1933
|
+
newSelection.add(itemId);
|
|
1711
1934
|
}
|
|
1712
1935
|
}
|
|
1713
1936
|
return newSelection;
|
|
1714
1937
|
});
|
|
1715
1938
|
}
|
|
1716
|
-
toggleSelectedItem(
|
|
1939
|
+
toggleSelectedItem(id) {
|
|
1717
1940
|
const table = this._table();
|
|
1718
|
-
const id = table.trackBy(item);
|
|
1719
1941
|
table.updateSelectedItems(selection => {
|
|
1720
|
-
const next = new
|
|
1942
|
+
const next = new Set(selection);
|
|
1721
1943
|
if (next.has(id)) {
|
|
1722
1944
|
next.delete(id);
|
|
1723
1945
|
}
|
|
@@ -1725,7 +1947,7 @@ class SciTableSelectionService {
|
|
|
1725
1947
|
if (table.selectable() === 'single') {
|
|
1726
1948
|
next.clear();
|
|
1727
1949
|
}
|
|
1728
|
-
next.
|
|
1950
|
+
next.add(id);
|
|
1729
1951
|
}
|
|
1730
1952
|
return next;
|
|
1731
1953
|
});
|
|
@@ -1775,6 +1997,7 @@ class SciTableCellComponent {
|
|
|
1775
1997
|
...(ngDevMode ? [{ debugName: "cell" }] : /* istanbul ignore next */ []));
|
|
1776
1998
|
row = input.required(/* @ts-ignore */
|
|
1777
1999
|
...(ngDevMode ? [{ debugName: "row" }] : /* istanbul ignore next */ []));
|
|
2000
|
+
_table = inject(ɵSCI_TABLE);
|
|
1778
2001
|
template = this.computeTemplate();
|
|
1779
2002
|
templateContext = this.computeTemplateContext();
|
|
1780
2003
|
partAttribute = this.computePartAttribute();
|
|
@@ -1807,6 +2030,7 @@ class SciTableCellComponent {
|
|
|
1807
2030
|
computePartAttribute() {
|
|
1808
2031
|
return computed(() => {
|
|
1809
2032
|
const cell = this.cell();
|
|
2033
|
+
const partType = this._table().type === 'tree' ? 'node' : 'row';
|
|
1810
2034
|
const referenceType = cell.type === 'enum' ? cell.column.referenceType : undefined;
|
|
1811
2035
|
return Arrays.distinct([
|
|
1812
2036
|
// Enable column styling based on column type. For enum columns, resolves to the reference type, if any, i.e., boolean or icon.
|
|
@@ -1814,11 +2038,11 @@ class SciTableCellComponent {
|
|
|
1814
2038
|
// Enable column styling based on column name.
|
|
1815
2039
|
cell.column.name,
|
|
1816
2040
|
// Enable row styling based on selection state.
|
|
1817
|
-
this.row().selected() ?
|
|
2041
|
+
this.row().selected() ? `${partType}:selected` : `${partType}:not:selected`,
|
|
1818
2042
|
// Enable row styling based on active state.
|
|
1819
|
-
this.row().active() ?
|
|
2043
|
+
this.row().active() ? `${partType}:active` : `${partType}:not:active`,
|
|
1820
2044
|
// Enable row styling based on row bindings.
|
|
1821
|
-
...Arrays.coerce(this.row().bindings
|
|
2045
|
+
...Arrays.coerce(this.row().bindings.parts()),
|
|
1822
2046
|
]).join(' ');
|
|
1823
2047
|
});
|
|
1824
2048
|
}
|
|
@@ -1876,27 +2100,35 @@ class SciTableRowComponent {
|
|
|
1876
2100
|
...(ngDevMode ? [{ debugName: "row" }] : /* istanbul ignore next */ []));
|
|
1877
2101
|
primaryAction = output();
|
|
1878
2102
|
table = inject(ɵSCI_TABLE);
|
|
2103
|
+
_selectionService = inject(SciTableSelectionService);
|
|
1879
2104
|
rowActionToolbar = viewChild(SciToolbarComponent, /* @ts-ignore */
|
|
1880
2105
|
...(ngDevMode ? [{ debugName: "rowActionToolbar" }] : /* istanbul ignore next */ []));
|
|
1881
2106
|
rowActionsToolbarName = `toolbar:${UUID.randomUUID()}`;
|
|
1882
|
-
_selectionService = inject(SciTableSelectionService);
|
|
1883
2107
|
constructor() {
|
|
1884
2108
|
this.contributeRowActions();
|
|
1885
2109
|
}
|
|
1886
2110
|
onRowClick(event) {
|
|
1887
|
-
|
|
2111
|
+
const row = this.row();
|
|
2112
|
+
if (row.id === skeletonRowId) {
|
|
1888
2113
|
return;
|
|
1889
2114
|
}
|
|
1890
|
-
|
|
2115
|
+
const index = this.table().rowIndexById().get(row.id) ?? -1;
|
|
2116
|
+
void this._selectionService.onRowClick(index, event);
|
|
1891
2117
|
}
|
|
1892
2118
|
onRowDblClick(event) {
|
|
1893
|
-
|
|
2119
|
+
const row = this.row();
|
|
2120
|
+
if (row.id === skeletonRowId || isFromInput(event) || isFromButton(event)) {
|
|
1894
2121
|
return;
|
|
1895
2122
|
}
|
|
1896
|
-
this.primaryAction.emit({ sourceEvent: event, items: [
|
|
2123
|
+
this.primaryAction.emit({ sourceEvent: event, items: [row.id] });
|
|
1897
2124
|
}
|
|
1898
2125
|
onRowMouseEnter() {
|
|
1899
|
-
this.
|
|
2126
|
+
const row = this.row();
|
|
2127
|
+
if (row.id === skeletonRowId) {
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
const index = this.table().rowIndexById().get(row.id) ?? -1;
|
|
2131
|
+
this.table().hoveredIndex.set(index);
|
|
1900
2132
|
}
|
|
1901
2133
|
onRowMouseLeave(event) {
|
|
1902
2134
|
const next = event.relatedTarget;
|
|
@@ -1907,18 +2139,30 @@ class SciTableRowComponent {
|
|
|
1907
2139
|
this.table().hoveredIndex.set(-1);
|
|
1908
2140
|
}
|
|
1909
2141
|
onActionToolbarClick(event) {
|
|
2142
|
+
const row = this.row();
|
|
2143
|
+
if (row.id === skeletonRowId) {
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
event.stopPropagation(); // prevent selecting the row
|
|
2147
|
+
this.table().setActiveItem(row.id);
|
|
2148
|
+
}
|
|
2149
|
+
onTreeNodeHandle(event) {
|
|
2150
|
+
const row = this.row();
|
|
2151
|
+
if (row.id === skeletonRowId) {
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
1910
2154
|
event.stopPropagation(); // prevent selecting the row
|
|
1911
|
-
this.table().
|
|
2155
|
+
this.table().toggleRow(row.id);
|
|
1912
2156
|
}
|
|
1913
2157
|
contributeRowActions() {
|
|
1914
2158
|
contributeMenu(this.rowActionsToolbarName, toolbar => {
|
|
1915
|
-
if (
|
|
2159
|
+
if (this.row().id !== skeletonRowId) {
|
|
1916
2160
|
this.table().rowActions?.(toolbar, this.row().item, this.row().index);
|
|
1917
2161
|
}
|
|
1918
2162
|
});
|
|
1919
2163
|
}
|
|
1920
2164
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableRowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1921
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: SciTableRowComponent, isStandalone: true, selector: "sci-table-row", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { primaryAction: "primaryAction" }, host: { listeners: { "click": "onRowClick($event)", "dblclick": "onRowDblClick($event)", "mouseenter": "onRowMouseEnter()", "mouseleave": "onRowMouseLeave($event)" }, properties: { "attr.data-active": "row().active() ? '' : null", "attr.data-selected": "row().selected() ? '' : null", "attr.data-hovered": "row().hovered() ? '' : null" } }, viewQueries: [{ propertyName: "rowActionToolbar", first: true, predicate: SciToolbarComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@let row = this.row();\n\n<!-- Row Icons -->\n@if (row.cells
|
|
2165
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: SciTableRowComponent, isStandalone: true, selector: "sci-table-row", inputs: { row: { classPropertyName: "row", publicName: "row", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { primaryAction: "primaryAction" }, host: { listeners: { "click": "onRowClick($event)", "dblclick": "onRowDblClick($event)", "mouseenter": "onRowMouseEnter()", "mouseleave": "onRowMouseLeave($event)" }, properties: { "attr.data-active": "row().active() ? '' : null", "attr.data-selected": "row().selected() ? '' : null", "attr.data-hovered": "row().hovered() ? '' : null", "attr.data-level": "row().level", "attr.data-state": "row().level !== undefined && row().hasChildren() ? (row().expanded() ? 'expanded' : 'collapsed') : null", "style.--\u0275sci-table-row-level": "row().level" } }, viewQueries: [{ propertyName: "rowActionToolbar", first: true, predicate: SciToolbarComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@let row = this.row();\n\n<!-- Handle to expand or collapse a tree node -->\n@if (row.level !== undefined) {\n <button class=\"tree-node-handle e2e-tree-node-handle\" (click)=\"onTreeNodeHandle($event)\" [attr.disabled]=\"!row.hasChildren() || null\">\n @if (row.expanded()) {\n <sci-icon class=\"e2e-expanded\">scion:chevron_down</sci-icon>\n } @else {\n <sci-icon class=\"e2e-collapsed\">scion:chevron_right</sci-icon>\n }\n </button>\n}\n\n<!-- Row Icons -->\n@if (row.cells.length) {\n @for (icon of row.bindings.icons(); track $index) {\n <sci-icon>{{icon}}</sci-icon>\n }\n}\n\n<!-- Table Cells -->\n@for (cell of row.cells; track cell.column.name) {\n <sci-table-cell [cell]=\"cell\" [row]=\"row\"/>\n} @empty {\n @for (column of table().columns(); track column.name) {\n <div class=\"skeleton e2e-skeleton\" [attr.data-column]=\"column.name\"></div>\n }\n}\n\n<!--Row Actions -->\n@let hasRowActions = !!table().rowActions;\n@let isRowHovered = table().hoveredIndex() === row.index;\n@let isRowActionMenuOpen = rowActionToolbar()?.menuOpen();\n\n@if (hasRowActions && (isRowHovered || isRowActionMenuOpen)) {\n <sci-toolbar [name]=\"rowActionsToolbarName\" (click)=\"onActionToolbarClick($event)\" class=\"row-actions\"/>\n}\n", styles: ["@charset \"UTF-8\";:host{display:inline-grid;grid-template-columns:subgrid;grid-auto-rows:var(--sci-table-row-height);align-items:stretch;-webkit-user-select:none;user-select:none;border-bottom:var(--\\275sci-table-border-size) solid var(--sci-table-gridline-color)}:host[data-hovered]{background-color:var(--sci-table-row-background-color-hover)}:host[data-selected]{background-color:var(--sci-table-row-background-color-selected);--\\275sci-table-row-selected: true}:host[data-hovered]:not([data-selected]){border-radius:var(--sci-table-row-border-radius)}:host:is(:first-child,:not([data-selected])+sci-table-row)[data-selected]{border-top-left-radius:var(--sci-table-row-border-radius);border-top-right-radius:var(--sci-table-row-border-radius)}:host[data-selected]:not(:has(+sci-table-row[data-selected])){border-bottom-left-radius:var(--sci-table-row-border-radius);border-bottom-right-radius:var(--sci-table-row-border-radius)}@container (style(--\\275sci-table-scrolling: true)) or (style(--\\275sci-table-resizing: true)){:host{pointer-events:none}}:host:has(>div.skeleton){pointer-events:none;align-items:center}:host:has(>div.skeleton)>div.skeleton{display:inline-block;position:relative;overflow:hidden;border-radius:var(--sci-corner-small);background-color:var(--sci-color-skeleton-subtle);opacity:0;animation-name:visiblity;animation-duration:.5s;animation-timing-function:ease-in;animation-fill-mode:forwards}:host:has(>div.skeleton)>div.skeleton:after{position:absolute;inset:0;transform:translate(-100%);background-image:linear-gradient(to right,transparent 0,color-mix(in srgb,light-dark(var(--sci-static-color-white),var(--sci-static-color-black)) 30%,transparent) 50%,transparent);animation:shimmer 2s infinite;content:\"\"}@keyframes shimmer{to{transform:translate(100%)}}@keyframes visiblity{to{opacity:1}}:host:has(>div.skeleton)>div.skeleton{height:1em;margin-inline:var(--\\275sci-table-cell-padding-inline)}:host>button.tree-node-handle{all:unset;display:inline-grid;position:relative;order:1}:host>button.tree-node-handle:disabled{display:none}:host>:is(sci-icon,button.tree-node-handle):has(~sci-table-cell:first-of-type){grid-column:1;grid-row:1;align-self:center;margin-left:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 24px + var(--\\275sci-table-row-level, 0) * var(--sci-table-row-indent));--sci-icon-size: 16px;width:auto;max-width:var(--sci-icon-size)}:host>:is(sci-icon,button.tree-node-handle):has(~sci-table-cell:first-of-type)+sci-table-cell{grid-column:1;grid-row:1;border-left-width:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 24px + var(--\\275sci-table-row-level, 0) * var(--sci-table-row-indent))}:host>:is(sci-icon,button.tree-node-handle):has(~sci-table-cell:first-of-type)+sci-table-cell[data-padding=false]{border-left-width:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 24px + var(--\\275sci-table-row-level, 0) * var(--sci-table-row-indent) - var(--\\275sci-table-cell-padding-inline))}:host>sci-toolbar.row-actions{position:sticky;right:0;transform:translate(calc(-1 * var(--\\275sci-table-cell-padding-inline)));justify-self:end;align-self:center;border-radius:var(--sci-corner);padding:1px;background-color:var(--\\275sci-table-row-action-background-color);z-index:2;--sci-toolbar-item-background-color-hover: color-mix(in srgb, var(--\\275sci-table-row-action-background-color) 85%, light-dark(var(--sci-static-color-black), var(--sci-static-color-white)));--sci-toolbar-item-background-color-active: color-mix(in srgb, var(--\\275sci-table-row-action-background-color) 75%, light-dark(var(--sci-static-color-black), var(--sci-static-color-white)))}@container not style(--\\275sci-table-row-selected: true){:host>sci-toolbar.row-actions{--\\275sci-table-row-action-background-color: var(--sci-table-row-action-background-color)}}@container style(--\\275sci-table-row-selected: true){:host>sci-toolbar.row-actions{--\\275sci-table-row-action-background-color: var(--sci-table-row-action-background-color-selected)}}:host>sci-toolbar.row-actions:empty{display:none}\n"], dependencies: [{ kind: "component", type: SciTableCellComponent, selector: "sci-table-cell", inputs: ["cell", "row"] }, { kind: "component", type: SciToolbarComponent, selector: "sci-toolbar", inputs: ["name", "orientation", "context", "acceleratorTarget", "popoverViewContainerRef"] }, { kind: "component", type: SciIconComponent, selector: "sci-icon" }] });
|
|
1922
2166
|
}
|
|
1923
2167
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableRowComponent, decorators: [{
|
|
1924
2168
|
type: Component,
|
|
@@ -1930,11 +2174,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
1930
2174
|
'[attr.data-active]': `row().active() ? '' : null`,
|
|
1931
2175
|
'[attr.data-selected]': `row().selected() ? '' : null`,
|
|
1932
2176
|
'[attr.data-hovered]': `row().hovered() ? '' : null`,
|
|
2177
|
+
'[attr.data-level]': 'row().level',
|
|
2178
|
+
'[attr.data-state]': `row().level !== undefined && row().hasChildren() ? (row().expanded() ? 'expanded' : 'collapsed') : null`,
|
|
2179
|
+
'[style.--ɵsci-table-row-level]': 'row().level', // only set if hierarchical
|
|
1933
2180
|
'(click)': 'onRowClick($event)',
|
|
1934
2181
|
'(dblclick)': 'onRowDblClick($event)',
|
|
1935
2182
|
'(mouseenter)': 'onRowMouseEnter()',
|
|
1936
2183
|
'(mouseleave)': 'onRowMouseLeave($event)',
|
|
1937
|
-
}, template: "@let row = this.row();\n\n<!-- Row Icons -->\n@if (row.cells
|
|
2184
|
+
}, template: "@let row = this.row();\n\n<!-- Handle to expand or collapse a tree node -->\n@if (row.level !== undefined) {\n <button class=\"tree-node-handle e2e-tree-node-handle\" (click)=\"onTreeNodeHandle($event)\" [attr.disabled]=\"!row.hasChildren() || null\">\n @if (row.expanded()) {\n <sci-icon class=\"e2e-expanded\">scion:chevron_down</sci-icon>\n } @else {\n <sci-icon class=\"e2e-collapsed\">scion:chevron_right</sci-icon>\n }\n </button>\n}\n\n<!-- Row Icons -->\n@if (row.cells.length) {\n @for (icon of row.bindings.icons(); track $index) {\n <sci-icon>{{icon}}</sci-icon>\n }\n}\n\n<!-- Table Cells -->\n@for (cell of row.cells; track cell.column.name) {\n <sci-table-cell [cell]=\"cell\" [row]=\"row\"/>\n} @empty {\n @for (column of table().columns(); track column.name) {\n <div class=\"skeleton e2e-skeleton\" [attr.data-column]=\"column.name\"></div>\n }\n}\n\n<!--Row Actions -->\n@let hasRowActions = !!table().rowActions;\n@let isRowHovered = table().hoveredIndex() === row.index;\n@let isRowActionMenuOpen = rowActionToolbar()?.menuOpen();\n\n@if (hasRowActions && (isRowHovered || isRowActionMenuOpen)) {\n <sci-toolbar [name]=\"rowActionsToolbarName\" (click)=\"onActionToolbarClick($event)\" class=\"row-actions\"/>\n}\n", styles: ["@charset \"UTF-8\";:host{display:inline-grid;grid-template-columns:subgrid;grid-auto-rows:var(--sci-table-row-height);align-items:stretch;-webkit-user-select:none;user-select:none;border-bottom:var(--\\275sci-table-border-size) solid var(--sci-table-gridline-color)}:host[data-hovered]{background-color:var(--sci-table-row-background-color-hover)}:host[data-selected]{background-color:var(--sci-table-row-background-color-selected);--\\275sci-table-row-selected: true}:host[data-hovered]:not([data-selected]){border-radius:var(--sci-table-row-border-radius)}:host:is(:first-child,:not([data-selected])+sci-table-row)[data-selected]{border-top-left-radius:var(--sci-table-row-border-radius);border-top-right-radius:var(--sci-table-row-border-radius)}:host[data-selected]:not(:has(+sci-table-row[data-selected])){border-bottom-left-radius:var(--sci-table-row-border-radius);border-bottom-right-radius:var(--sci-table-row-border-radius)}@container (style(--\\275sci-table-scrolling: true)) or (style(--\\275sci-table-resizing: true)){:host{pointer-events:none}}:host:has(>div.skeleton){pointer-events:none;align-items:center}:host:has(>div.skeleton)>div.skeleton{display:inline-block;position:relative;overflow:hidden;border-radius:var(--sci-corner-small);background-color:var(--sci-color-skeleton-subtle);opacity:0;animation-name:visiblity;animation-duration:.5s;animation-timing-function:ease-in;animation-fill-mode:forwards}:host:has(>div.skeleton)>div.skeleton:after{position:absolute;inset:0;transform:translate(-100%);background-image:linear-gradient(to right,transparent 0,color-mix(in srgb,light-dark(var(--sci-static-color-white),var(--sci-static-color-black)) 30%,transparent) 50%,transparent);animation:shimmer 2s infinite;content:\"\"}@keyframes shimmer{to{transform:translate(100%)}}@keyframes visiblity{to{opacity:1}}:host:has(>div.skeleton)>div.skeleton{height:1em;margin-inline:var(--\\275sci-table-cell-padding-inline)}:host>button.tree-node-handle{all:unset;display:inline-grid;position:relative;order:1}:host>button.tree-node-handle:disabled{display:none}:host>:is(sci-icon,button.tree-node-handle):has(~sci-table-cell:first-of-type){grid-column:1;grid-row:1;align-self:center;margin-left:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 24px + var(--\\275sci-table-row-level, 0) * var(--sci-table-row-indent));--sci-icon-size: 16px;width:auto;max-width:var(--sci-icon-size)}:host>:is(sci-icon,button.tree-node-handle):has(~sci-table-cell:first-of-type)+sci-table-cell{grid-column:1;grid-row:1;border-left-width:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 24px + var(--\\275sci-table-row-level, 0) * var(--sci-table-row-indent))}:host>:is(sci-icon,button.tree-node-handle):has(~sci-table-cell:first-of-type)+sci-table-cell[data-padding=false]{border-left-width:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 24px + var(--\\275sci-table-row-level, 0) * var(--sci-table-row-indent) - var(--\\275sci-table-cell-padding-inline))}:host>sci-toolbar.row-actions{position:sticky;right:0;transform:translate(calc(-1 * var(--\\275sci-table-cell-padding-inline)));justify-self:end;align-self:center;border-radius:var(--sci-corner);padding:1px;background-color:var(--\\275sci-table-row-action-background-color);z-index:2;--sci-toolbar-item-background-color-hover: color-mix(in srgb, var(--\\275sci-table-row-action-background-color) 85%, light-dark(var(--sci-static-color-black), var(--sci-static-color-white)));--sci-toolbar-item-background-color-active: color-mix(in srgb, var(--\\275sci-table-row-action-background-color) 75%, light-dark(var(--sci-static-color-black), var(--sci-static-color-white)))}@container not style(--\\275sci-table-row-selected: true){:host>sci-toolbar.row-actions{--\\275sci-table-row-action-background-color: var(--sci-table-row-action-background-color)}}@container style(--\\275sci-table-row-selected: true){:host>sci-toolbar.row-actions{--\\275sci-table-row-action-background-color: var(--sci-table-row-action-background-color-selected)}}:host>sci-toolbar.row-actions:empty{display:none}\n"] }]
|
|
1938
2185
|
}], ctorParameters: () => [], propDecorators: { row: [{ type: i0.Input, args: [{ isSignal: true, alias: "row", required: true }] }], primaryAction: [{ type: i0.Output, args: ["primaryAction"] }], rowActionToolbar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SciToolbarComponent), { isSignal: true }] }] } });
|
|
1939
2186
|
|
|
1940
2187
|
/*
|
|
@@ -1947,27 +2194,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
1947
2194
|
* SPDX-License-Identifier: EPL-2.0
|
|
1948
2195
|
*/
|
|
1949
2196
|
class SciTableKeyboardNavigatorDirective {
|
|
2197
|
+
_table = inject(ɵSCI_TABLE);
|
|
1950
2198
|
selectionService = inject(SciTableSelectionService);
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
return true;
|
|
2199
|
+
isFromInput = isFromInput;
|
|
2200
|
+
isFromButton = isFromButton;
|
|
2201
|
+
async onKeydown(event) {
|
|
2202
|
+
if (event.key === '+' && event.location === 3) { // NumpadAdd
|
|
2203
|
+
void this.onNumpadAdd(event);
|
|
1957
2204
|
}
|
|
1958
|
-
if (event.
|
|
1959
|
-
|
|
2205
|
+
else if (event.key === '-' && event.location === 3) { // NumpadSubtract
|
|
2206
|
+
void this.onNumpadSubtract(event);
|
|
1960
2207
|
}
|
|
1961
|
-
return false;
|
|
1962
2208
|
}
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2209
|
+
async onNumpadAdd(event) {
|
|
2210
|
+
event.preventDefault();
|
|
2211
|
+
const table = this._table();
|
|
2212
|
+
if (event.ctrlKey || event.metaKey) {
|
|
2213
|
+
table.expandAll();
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
const activeRow = table.activeRow();
|
|
2217
|
+
if (activeRow?.id !== skeletonRowId && activeRow?.hasChildren()) {
|
|
2218
|
+
table.expand(activeRow.id);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
async onNumpadSubtract(event) {
|
|
2222
|
+
event.preventDefault();
|
|
2223
|
+
const table = this._table();
|
|
2224
|
+
if (event.ctrlKey || event.metaKey) {
|
|
2225
|
+
table.collapseAll();
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
const activeRow = table.activeRow();
|
|
2229
|
+
if (activeRow?.id !== skeletonRowId && activeRow?.hasChildren()) {
|
|
2230
|
+
table.collapse(activeRow.id);
|
|
2231
|
+
}
|
|
1968
2232
|
}
|
|
1969
2233
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableKeyboardNavigatorDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1970
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: SciTableKeyboardNavigatorDirective, isStandalone: true, selector: "[sciTableKeyboardNavigator]", host: { listeners: { "keydown.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.shift.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.control.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.meta.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.shift.control.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.shift.meta.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.shift.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.control.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.meta.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.shift.control.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.shift.meta.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.space": "isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)", "keydown.control.space": "isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)", "keydown.meta.space": "isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)", "keydown.control.a": "isFromInput($event) || selectionService.onControlA($event)", "keydown.meta.a": "isFromInput($event) || selectionService.onControlA($event)", "keydown.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.shift.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.control.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.shift.control.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.shift.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.control.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.shift.control.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.shift.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.control.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.shift.control.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown.shift.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown.control.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown.shift.control.end": "isFromInput($event) || selectionService.onEnd($event)" }, properties: { "attr.tabindex": "0" } }, ngImport: i0 });
|
|
2234
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: SciTableKeyboardNavigatorDirective, isStandalone: true, selector: "[sciTableKeyboardNavigator]", host: { listeners: { "keydown.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.shift.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.control.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.meta.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.shift.control.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.shift.meta.arrowUp": "isFromInput($event) || selectionService.onArrowUp($event)", "keydown.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.shift.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.control.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.meta.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.shift.control.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.shift.meta.arrowDown": "isFromInput($event) || selectionService.onArrowDown($event)", "keydown.arrowLeft": "isFromInput($event) || selectionService.onArrowLeft($event)", "keydown.arrowRight": "isFromInput($event) || selectionService.onArrowRight($event)", "keydown.space": "isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)", "keydown.control.space": "isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)", "keydown.meta.space": "isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)", "keydown.control.a": "isFromInput($event) || selectionService.onControlA($event)", "keydown.meta.a": "isFromInput($event) || selectionService.onControlA($event)", "keydown.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.shift.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.control.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.meta.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.shift.control.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.shift.meta.pageUp": "isFromInput($event) || selectionService.onPageUp($event)", "keydown.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.shift.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.control.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.meta.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.shift.control.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.shift.meta.pageDown": "isFromInput($event) || selectionService.onPageDown($event)", "keydown.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.shift.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.control.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.meta.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.shift.control.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.shift.meta.home": "isFromInput($event) || selectionService.onHome($event)", "keydown.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown.shift.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown.control.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown.meta.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown.shift.control.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown.shift.meta.end": "isFromInput($event) || selectionService.onEnd($event)", "keydown": "isFromInput($event) || onKeydown($event)" }, properties: { "attr.tabindex": "0" } }, ngImport: i0 });
|
|
1971
2235
|
}
|
|
1972
2236
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableKeyboardNavigatorDirective, decorators: [{
|
|
1973
2237
|
type: Directive,
|
|
@@ -1987,6 +2251,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
1987
2251
|
'(keydown.meta.arrowDown)': 'isFromInput($event) || selectionService.onArrowDown($event)',
|
|
1988
2252
|
'(keydown.shift.control.arrowDown)': 'isFromInput($event) || selectionService.onArrowDown($event)',
|
|
1989
2253
|
'(keydown.shift.meta.arrowDown)': 'isFromInput($event) || selectionService.onArrowDown($event)',
|
|
2254
|
+
'(keydown.arrowLeft)': 'isFromInput($event) || selectionService.onArrowLeft($event)',
|
|
2255
|
+
'(keydown.arrowRight)': 'isFromInput($event) || selectionService.onArrowRight($event)',
|
|
1990
2256
|
'(keydown.space)': 'isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)',
|
|
1991
2257
|
'(keydown.control.space)': 'isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)',
|
|
1992
2258
|
'(keydown.meta.space)': 'isFromInput($event) || isFromButton($event) || selectionService.onSpace($event)',
|
|
@@ -1995,19 +2261,28 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
1995
2261
|
'(keydown.pageUp)': 'isFromInput($event) || selectionService.onPageUp($event)',
|
|
1996
2262
|
'(keydown.shift.pageUp)': 'isFromInput($event) || selectionService.onPageUp($event)',
|
|
1997
2263
|
'(keydown.control.pageUp)': 'isFromInput($event) || selectionService.onPageUp($event)',
|
|
2264
|
+
'(keydown.meta.pageUp)': 'isFromInput($event) || selectionService.onPageUp($event)',
|
|
1998
2265
|
'(keydown.shift.control.pageUp)': 'isFromInput($event) || selectionService.onPageUp($event)',
|
|
2266
|
+
'(keydown.shift.meta.pageUp)': 'isFromInput($event) || selectionService.onPageUp($event)',
|
|
1999
2267
|
'(keydown.pageDown)': 'isFromInput($event) || selectionService.onPageDown($event)',
|
|
2000
2268
|
'(keydown.shift.pageDown)': 'isFromInput($event) || selectionService.onPageDown($event)',
|
|
2001
2269
|
'(keydown.control.pageDown)': 'isFromInput($event) || selectionService.onPageDown($event)',
|
|
2270
|
+
'(keydown.meta.pageDown)': 'isFromInput($event) || selectionService.onPageDown($event)',
|
|
2002
2271
|
'(keydown.shift.control.pageDown)': 'isFromInput($event) || selectionService.onPageDown($event)',
|
|
2272
|
+
'(keydown.shift.meta.pageDown)': 'isFromInput($event) || selectionService.onPageDown($event)',
|
|
2003
2273
|
'(keydown.home)': 'isFromInput($event) || selectionService.onHome($event)',
|
|
2004
2274
|
'(keydown.shift.home)': 'isFromInput($event) || selectionService.onHome($event)',
|
|
2005
2275
|
'(keydown.control.home)': 'isFromInput($event) || selectionService.onHome($event)',
|
|
2276
|
+
'(keydown.meta.home)': 'isFromInput($event) || selectionService.onHome($event)',
|
|
2006
2277
|
'(keydown.shift.control.home)': 'isFromInput($event) || selectionService.onHome($event)',
|
|
2278
|
+
'(keydown.shift.meta.home)': 'isFromInput($event) || selectionService.onHome($event)',
|
|
2007
2279
|
'(keydown.end)': 'isFromInput($event) || selectionService.onEnd($event)',
|
|
2008
2280
|
'(keydown.shift.end)': 'isFromInput($event) || selectionService.onEnd($event)',
|
|
2009
2281
|
'(keydown.control.end)': 'isFromInput($event) || selectionService.onEnd($event)',
|
|
2282
|
+
'(keydown.meta.end)': 'isFromInput($event) || selectionService.onEnd($event)',
|
|
2010
2283
|
'(keydown.shift.control.end)': 'isFromInput($event) || selectionService.onEnd($event)',
|
|
2284
|
+
'(keydown.shift.meta.end)': 'isFromInput($event) || selectionService.onEnd($event)',
|
|
2285
|
+
'(keydown)': 'isFromInput($event) || onKeydown($event)',
|
|
2011
2286
|
},
|
|
2012
2287
|
}]
|
|
2013
2288
|
}] });
|
|
@@ -2356,7 +2631,7 @@ class SciColumnHeaderComponent {
|
|
|
2356
2631
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciColumnHeaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2357
2632
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: SciColumnHeaderComponent, isStandalone: true, selector: "sci-column-header", inputs: { column: { classPropertyName: "column", publicName: "column", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "attr.data-column": "column().name" } }, providers: [
|
|
2358
2633
|
configureTooltip$1(),
|
|
2359
|
-
], viewQueries: [{ propertyName: "labelElement", first: true, predicate: ["label"], descendants: true, isSignal: true }], hostDirectives: [{ directive: i1.SciTooltipOnOverflowDirective }], ngImport: i0, template: "@if (column().sortable()) {\n <button class=\"sort e2e-sort\"\n [attr.data-sort-direction]=\"sortDirection()\"\n (click)=\"onSort($event)\">\n <!-- Header -->\n <ng-container *ngTemplateOutlet=\"header_template\"/>\n\n <!-- Sort Indicator -->\n @switch (sortDirection()) {\n @case ('asc') {\n <sci-icon>scion:sort_ascending</sci-icon>\n }\n @case ('desc') {\n <sci-icon>scion:sort_descending</sci-icon>\n }\n @default {\n <sci-icon class=\"no-sort\">scion:sort_indicator</sci-icon>\n }\n }\n </button>\n} @else {\n <ng-container *ngTemplateOutlet=\"header_template\"/>\n}\n\n<ng-template #header_template>\n @if (column().header.icon) {\n <sci-icon class=\"icon e2e-icon\">{{column().header.icon}}</sci-icon>\n }\n\n @if (column().header.label) {\n <span [class.wrap]=\"table().wrapHeader()\" class=\"label\" #label>\n {{column().header.label!()}}\n </span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:grid;align-items:center;gap:.25em}:host:not(:has(button.sort)){padding-inline:var(--\\275sci-table-cell-padding-inline)}:host:is(:focus-visible,:hover){--\\275sci-table-column-header-hover: true}:host:has(>sci-icon.icon):has(>span.label){grid-template-columns:auto 1fr}:host>button.sort{all:unset;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.25em;padding-inline:var(--\\275sci-table-cell-padding-inline);cursor:var(--sci-table-header-cursor);align-self:stretch}:host>button.sort:has(>sci-icon.icon):has(>span.label){grid-template-columns:auto minmax(0,1fr) auto}@container not style(--\\275sci-table-column-header-hover: true){:host>button.sort>sci-icon.no-sort{visibility:hidden}}@container style(--\\275sci-table-column-header-hover: true){:host>button.sort{background-color:var(--sci-table-header-background-color-hover)}}:host span.label{display:inline;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--sci-table-header-text-color);font-family:var(--sci-table-header-font-family),sans-serif;font-weight:var(--sci-table-header-font-weight);font-size:var(--sci-table-header-font-size);padding-block:.25em}:host span.label.wrap{white-space:wrap}\n"], dependencies: [{ kind: "component", type: SciIconComponent, selector: "sci-icon" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
2634
|
+
], viewQueries: [{ propertyName: "labelElement", first: true, predicate: ["label"], descendants: true, isSignal: true }], hostDirectives: [{ directive: i1.SciTooltipOnOverflowDirective }], ngImport: i0, template: "@if (column().sortable()) {\n <button class=\"sort e2e-sort\"\n [attr.data-sort-direction]=\"sortDirection()\"\n (click)=\"onSort($event)\">\n <!-- Header -->\n <ng-container *ngTemplateOutlet=\"header_template\"/>\n\n <!-- Sort Indicator -->\n @switch (sortDirection()) {\n @case ('asc') {\n <sci-icon>scion:sort_ascending</sci-icon>\n }\n @case ('desc') {\n <sci-icon>scion:sort_descending</sci-icon>\n }\n @default {\n <sci-icon class=\"no-sort\">scion:sort_indicator</sci-icon>\n }\n }\n </button>\n} @else {\n <ng-container *ngTemplateOutlet=\"header_template\"/>\n}\n\n<ng-template #header_template>\n @if (column().header.icon) {\n <sci-icon class=\"icon e2e-icon\">{{column().header.icon}}</sci-icon>\n }\n\n @if (column().header.label) {\n <span [class.wrap]=\"table().wrapHeader()\" class=\"label\" #label>\n {{column().header.label!()}}\n </span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:grid;align-items:center;gap:.25em}:host:not(:has(button.sort)){padding-inline:var(--\\275sci-table-cell-padding-inline)}:host:is(:focus-visible,:hover){--\\275sci-table-column-header-hover: true}:host:has(>sci-icon.icon):has(>span.label){grid-template-columns:auto 1fr}:host>button.sort{all:unset;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.25em;padding-inline:var(--\\275sci-table-cell-padding-inline);cursor:var(--sci-table-header-cursor);align-self:stretch}:host>button.sort:has(>sci-icon.icon):has(>span.label){grid-template-columns:auto minmax(0,1fr) auto}:host>button.sort:not(:has(>:is(sci-icon.icon,span.label))){grid-template-columns:1fr;justify-items:end}@container not style(--\\275sci-table-column-header-hover: true){:host>button.sort>sci-icon.no-sort{visibility:hidden}}@container style(--\\275sci-table-column-header-hover: true){:host>button.sort{background-color:var(--sci-table-header-background-color-hover)}}:host span.label{display:inline;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--sci-table-header-text-color);font-family:var(--sci-table-header-font-family),sans-serif;font-weight:var(--sci-table-header-font-weight);font-size:var(--sci-table-header-font-size);padding-block:.25em}:host span.label.wrap{white-space:wrap}\n"], dependencies: [{ kind: "component", type: SciIconComponent, selector: "sci-icon" }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
|
|
2360
2635
|
}
|
|
2361
2636
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciColumnHeaderComponent, decorators: [{
|
|
2362
2637
|
type: Component,
|
|
@@ -2369,7 +2644,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
2369
2644
|
SciTooltipOnOverflowDirective,
|
|
2370
2645
|
], providers: [
|
|
2371
2646
|
configureTooltip$1(),
|
|
2372
|
-
], template: "@if (column().sortable()) {\n <button class=\"sort e2e-sort\"\n [attr.data-sort-direction]=\"sortDirection()\"\n (click)=\"onSort($event)\">\n <!-- Header -->\n <ng-container *ngTemplateOutlet=\"header_template\"/>\n\n <!-- Sort Indicator -->\n @switch (sortDirection()) {\n @case ('asc') {\n <sci-icon>scion:sort_ascending</sci-icon>\n }\n @case ('desc') {\n <sci-icon>scion:sort_descending</sci-icon>\n }\n @default {\n <sci-icon class=\"no-sort\">scion:sort_indicator</sci-icon>\n }\n }\n </button>\n} @else {\n <ng-container *ngTemplateOutlet=\"header_template\"/>\n}\n\n<ng-template #header_template>\n @if (column().header.icon) {\n <sci-icon class=\"icon e2e-icon\">{{column().header.icon}}</sci-icon>\n }\n\n @if (column().header.label) {\n <span [class.wrap]=\"table().wrapHeader()\" class=\"label\" #label>\n {{column().header.label!()}}\n </span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:grid;align-items:center;gap:.25em}:host:not(:has(button.sort)){padding-inline:var(--\\275sci-table-cell-padding-inline)}:host:is(:focus-visible,:hover){--\\275sci-table-column-header-hover: true}:host:has(>sci-icon.icon):has(>span.label){grid-template-columns:auto 1fr}:host>button.sort{all:unset;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.25em;padding-inline:var(--\\275sci-table-cell-padding-inline);cursor:var(--sci-table-header-cursor);align-self:stretch}:host>button.sort:has(>sci-icon.icon):has(>span.label){grid-template-columns:auto minmax(0,1fr) auto}@container not style(--\\275sci-table-column-header-hover: true){:host>button.sort>sci-icon.no-sort{visibility:hidden}}@container style(--\\275sci-table-column-header-hover: true){:host>button.sort{background-color:var(--sci-table-header-background-color-hover)}}:host span.label{display:inline;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--sci-table-header-text-color);font-family:var(--sci-table-header-font-family),sans-serif;font-weight:var(--sci-table-header-font-weight);font-size:var(--sci-table-header-font-size);padding-block:.25em}:host span.label.wrap{white-space:wrap}\n"] }]
|
|
2647
|
+
], template: "@if (column().sortable()) {\n <button class=\"sort e2e-sort\"\n [attr.data-sort-direction]=\"sortDirection()\"\n (click)=\"onSort($event)\">\n <!-- Header -->\n <ng-container *ngTemplateOutlet=\"header_template\"/>\n\n <!-- Sort Indicator -->\n @switch (sortDirection()) {\n @case ('asc') {\n <sci-icon>scion:sort_ascending</sci-icon>\n }\n @case ('desc') {\n <sci-icon>scion:sort_descending</sci-icon>\n }\n @default {\n <sci-icon class=\"no-sort\">scion:sort_indicator</sci-icon>\n }\n }\n </button>\n} @else {\n <ng-container *ngTemplateOutlet=\"header_template\"/>\n}\n\n<ng-template #header_template>\n @if (column().header.icon) {\n <sci-icon class=\"icon e2e-icon\">{{column().header.icon}}</sci-icon>\n }\n\n @if (column().header.label) {\n <span [class.wrap]=\"table().wrapHeader()\" class=\"label\" #label>\n {{column().header.label!()}}\n </span>\n }\n</ng-template>\n", styles: ["@charset \"UTF-8\";:host{display:grid;align-items:center;gap:.25em}:host:not(:has(button.sort)){padding-inline:var(--\\275sci-table-cell-padding-inline)}:host:is(:focus-visible,:hover){--\\275sci-table-column-header-hover: true}:host:has(>sci-icon.icon):has(>span.label){grid-template-columns:auto 1fr}:host>button.sort{all:unset;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:.25em;padding-inline:var(--\\275sci-table-cell-padding-inline);cursor:var(--sci-table-header-cursor);align-self:stretch}:host>button.sort:has(>sci-icon.icon):has(>span.label){grid-template-columns:auto minmax(0,1fr) auto}:host>button.sort:not(:has(>:is(sci-icon.icon,span.label))){grid-template-columns:1fr;justify-items:end}@container not style(--\\275sci-table-column-header-hover: true){:host>button.sort>sci-icon.no-sort{visibility:hidden}}@container style(--\\275sci-table-column-header-hover: true){:host>button.sort{background-color:var(--sci-table-header-background-color-hover)}}:host span.label{display:inline;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--sci-table-header-text-color);font-family:var(--sci-table-header-font-family),sans-serif;font-weight:var(--sci-table-header-font-weight);font-size:var(--sci-table-header-font-size);padding-block:.25em}:host span.label.wrap{white-space:wrap}\n"] }]
|
|
2373
2648
|
}], propDecorators: { column: [{ type: i0.Input, args: [{ isSignal: true, alias: "column", required: true }] }], labelElement: [{ type: i0.ViewChild, args: ['label', { isSignal: true }] }] } });
|
|
2374
2649
|
function configureTooltip$1() {
|
|
2375
2650
|
return [
|
|
@@ -2598,6 +2873,72 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
2598
2873
|
* SPDX-License-Identifier: EPL-2.0
|
|
2599
2874
|
*/
|
|
2600
2875
|
/**
|
|
2876
|
+
* Provides a table to display tabular or hierarchical data.
|
|
2877
|
+
*
|
|
2878
|
+
* ### Usage
|
|
2879
|
+
*
|
|
2880
|
+
* Create a table using the `table()` function, passing a signal containing the items to display in the table and a factory function to define the columns.
|
|
2881
|
+
* Alternatively, pass a descriptor to display a tree structure, configure a custom datasource, or set additional options.
|
|
2882
|
+
*
|
|
2883
|
+
* ```ts
|
|
2884
|
+
* import {SciTableComponent, table} from '@scion/components/table';
|
|
2885
|
+
* import {httpResource} from '@angular/common/http';
|
|
2886
|
+
*
|
|
2887
|
+
* // Load data to display in the table.
|
|
2888
|
+
* const users = httpResource<User[]>(() => '/api/users', {defaultValue: []});
|
|
2889
|
+
*
|
|
2890
|
+
* // Create the table and pass data.
|
|
2891
|
+
* const userTable = table(users.value, table => table
|
|
2892
|
+
* .addNumberColumn('ID', user => user.id)
|
|
2893
|
+
* .addStringColumn('Name', user => user.name)
|
|
2894
|
+
* .addBooleanColumn('Inactive', user => user.inactive)
|
|
2895
|
+
* .addDateColumn('Expiration', user => user.expiration),
|
|
2896
|
+
* );
|
|
2897
|
+
* ```
|
|
2898
|
+
*
|
|
2899
|
+
* In the HTML, bind the created table instance to `<sci-table>` and assign it a name.
|
|
2900
|
+
* The name must start with the `table:` prefix and is used to store user settings.
|
|
2901
|
+
*
|
|
2902
|
+
* ```html
|
|
2903
|
+
* <sci-table name="table:users" [table]="userTable"/>
|
|
2904
|
+
* ```
|
|
2905
|
+
*
|
|
2906
|
+
* ### Fixed Row Height
|
|
2907
|
+
*
|
|
2908
|
+
* All rows in the table must have equal height, which is `2em` by default. The height can be changed per table using the CSS variable `--sci-table-row-height`.
|
|
2909
|
+
*
|
|
2910
|
+
* ```scss
|
|
2911
|
+
* sci-table {
|
|
2912
|
+
* --sci-table-row-height: 30px;
|
|
2913
|
+
* }
|
|
2914
|
+
* ```
|
|
2915
|
+
*
|
|
2916
|
+
* ### Styling
|
|
2917
|
+
*
|
|
2918
|
+
* The appearance of the table can be customized using the following CSS variables, either globally in the `:root` or at the table level.
|
|
2919
|
+
*
|
|
2920
|
+
* - `--sci-table-gridline-color`
|
|
2921
|
+
* - `--sci-table-header-height`
|
|
2922
|
+
* - `--sci-table-header-cursor`
|
|
2923
|
+
* - `--sci-table-header-background-color`
|
|
2924
|
+
* - `--sci-table-header-background-color-hover`
|
|
2925
|
+
* - `--sci-table-header-font-family`
|
|
2926
|
+
* - `--sci-table-header-font-size`
|
|
2927
|
+
* - `--sci-table-header-font-weight`
|
|
2928
|
+
* - `--sci-table-header-text-color`
|
|
2929
|
+
* - `--sci-table-header-column-divider`
|
|
2930
|
+
* - `--sci-table-row-height`
|
|
2931
|
+
* - `--sci-table-row-indent`
|
|
2932
|
+
* - `--sci-table-row-background-color-hover`
|
|
2933
|
+
* - `--sci-table-row-background-color-selected`
|
|
2934
|
+
* - `--sci-table-row-border-radius`
|
|
2935
|
+
* - `--sci-table-row-outline-color`
|
|
2936
|
+
* - `--sci-table-row-outline-width`
|
|
2937
|
+
* - `--sci-table-row-outline-style`
|
|
2938
|
+
* - `--sci-table-row-action-background-color`
|
|
2939
|
+
* - `--sci-table-row-action-background-color-selected`
|
|
2940
|
+
* - `--sci-table-cell-padding-inline`
|
|
2941
|
+
*
|
|
2601
2942
|
* @experimental since 22.3.0; API and behavior may change in any version without notice.
|
|
2602
2943
|
*/
|
|
2603
2944
|
class SciTableComponent {
|
|
@@ -2630,9 +2971,10 @@ class SciTableComponent {
|
|
|
2630
2971
|
}
|
|
2631
2972
|
onRowPrimaryAction(event) {
|
|
2632
2973
|
this.primaryAction.emit(event);
|
|
2974
|
+
event.items.forEach(item => this.table().toggleRow(item));
|
|
2633
2975
|
}
|
|
2634
2976
|
onRetry() {
|
|
2635
|
-
this.table().
|
|
2977
|
+
this.table().reloadFailed();
|
|
2636
2978
|
}
|
|
2637
2979
|
connectToModel() {
|
|
2638
2980
|
const viewportDimension = dimension(this._viewport);
|
|
@@ -2663,15 +3005,16 @@ class SciTableComponent {
|
|
|
2663
3005
|
}
|
|
2664
3006
|
scrollActiveRowIntoViewport() {
|
|
2665
3007
|
effect(() => {
|
|
2666
|
-
const
|
|
2667
|
-
if (
|
|
3008
|
+
const activeItem = this.table().activeItem();
|
|
3009
|
+
if (activeItem === undefined) {
|
|
2668
3010
|
return;
|
|
2669
3011
|
}
|
|
2670
3012
|
untracked(() => {
|
|
2671
3013
|
const viewport = this._viewport().nativeElement;
|
|
2672
3014
|
const viewportHeight = this.table().tableViewRef()?.viewportHeight() ?? 0;
|
|
2673
3015
|
const itemHeight = this.table().tableViewRef()?.itemHeight() ?? 0;
|
|
2674
|
-
const
|
|
3016
|
+
const activeIndex = this.table().rowIndexById().get(activeItem) ?? -1;
|
|
3017
|
+
const activeRowTop = activeIndex * itemHeight;
|
|
2675
3018
|
const activeRowBottom = activeRowTop + itemHeight;
|
|
2676
3019
|
const scrollTop = viewport.scrollTop;
|
|
2677
3020
|
const scrollBottom = scrollTop + viewportHeight;
|
|
@@ -2688,7 +3031,7 @@ class SciTableComponent {
|
|
|
2688
3031
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: SciTableComponent, isStandalone: true, selector: "sci-table", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, table: { classPropertyName: "table", publicName: "table", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { primaryAction: "primaryAction" }, host: { properties: { "attr.name": "name()", "style.--\u0275sci-table-scrolling": "table().scrolling() ? `true` : null", "style.--\u0275sci-table-resizing": "table().resizing() ? `true` : null" } }, providers: [
|
|
2689
3032
|
provideSciTable(),
|
|
2690
3033
|
SciTableSelectionService,
|
|
2691
|
-
], viewQueries: [{ propertyName: "_viewport", first: true, predicate: SciTableViewportComponent, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "_viewportClient", first: true, predicate: SciTableGridComponent, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "_tableHeader", first: true, predicate: SciTableHeaderComponent, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "_tableBody", first: true, predicate: SciTableBodyComponent, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "_itemSizeSyntheticElement", first: true, predicate: ["item_size_synthetic_element"], descendants: true, isSignal: true }, { propertyName: "_cellPaddingSyntheticElement", first: true, predicate: ["cell_padding_synthetic_element"], descendants: true, isSignal: true }, { propertyName: "rows", predicate: SciTableRowComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@let table = this.table();\n\n<!-- TODO [#384] Replace with sci-viewport -->\n<sci-table-viewport #viewport>\n <sci-table-grid>\n <!-- Table header -->\n @if (table.showHeader() || table.filterable()) {\n <sci-table-header/>\n }\n\n <!-- Table body -->\n <sci-table-body sciTableKeyboardNavigator>\n @if (!table.error()) {\n <!-- Track index to reuse the table row elements -->\n @for (row of table.rows(); track $index) {\n <sci-table-row [row]=\"row\" [sciAttributes]=\"row.bindings
|
|
3034
|
+
], viewQueries: [{ propertyName: "_viewport", first: true, predicate: SciTableViewportComponent, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "_viewportClient", first: true, predicate: SciTableGridComponent, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "_tableHeader", first: true, predicate: SciTableHeaderComponent, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "_tableBody", first: true, predicate: SciTableBodyComponent, descendants: true, read: ElementRef, isSignal: true }, { propertyName: "_itemSizeSyntheticElement", first: true, predicate: ["item_size_synthetic_element"], descendants: true, isSignal: true }, { propertyName: "_cellPaddingSyntheticElement", first: true, predicate: ["cell_padding_synthetic_element"], descendants: true, isSignal: true }, { propertyName: "rows", predicate: SciTableRowComponent, descendants: true, isSignal: true }], ngImport: i0, template: "@let table = this.table();\n\n<!-- TODO [#384] Replace with sci-viewport -->\n<sci-table-viewport #viewport>\n <sci-table-grid>\n <!-- Table header -->\n @if (table.showHeader() || table.filterable()) {\n <sci-table-header/>\n }\n\n <!-- Table body -->\n <sci-table-body sciTableKeyboardNavigator>\n @if (!table.error()) {\n <!-- Track index to reuse the table row elements -->\n @for (row of table.rows(); track $index) {\n <sci-table-row [row]=\"row\" [sciAttributes]=\"row.bindings.attributes()\" [class]=\"row.bindings.classes()\" (primaryAction)=\"onRowPrimaryAction($event)\"/>\n } @empty {\n <span class=\"message e2e-no-data\">{{('%scion:components:no_items.message' | sciText)()}}</span>\n }\n } @else {\n <span class=\"message e2e-datasource\">\n {{('%scion:components:datasource_error.message' | sciText)()}}\n\n <button (click)=\"onRetry()\" class=\"action e2e-datasource-retry\">\n <sci-icon>scion:reload</sci-icon>\n <span>{{('%scion:components:retry.action' | sciText)()}}</span>\n </button>\n </span>\n }\n\n <!-- Render active row outline separately to preserve row border-radius within block selection. -->\n @if (table.activeRow()) {\n <div class=\"active-row-outline e2e-active-row-outline\"></div>\n }\n </sci-table-body>\n\n <!-- Render column splitters. -->\n <sci-table-columns [rows]=\"rows()\"/>\n </sci-table-grid>\n</sci-table-viewport>\n\n<!-- Scrollbars -->\n<sci-scrollbar direction=\"hscroll\" [viewport]=\"viewport.host\"/>\n<sci-scrollbar direction=\"vscroll\" [viewport]=\"viewport.host\"/>\n\n@if (table.loading()) {\n <sci-throbber type=\"spinner\" class=\"loading-indicator e2e-loading-indicator\"/>\n}\n\n<!-- Synthetic element to compute the absolute pixel value of the `--sci-table-row-height`, relative to the table's font size if em-based. -->\n<div class=\"synthetic item-size\" #item_size_synthetic_element></div>\n\n<!-- Synthetic element to compute the absolute pixel value of the `--sci-table-cell-padding-inline`, relative to the table's font size if em-based. -->\n<div class=\"synthetic cell-padding\" #cell_padding_synthetic_element></div>\n", styles: ["@charset \"UTF-8\";:host{display:block;overflow:hidden;--\\275sci-table-border-size: 1px;--\\275sci-table-border-color: var(--sci-color-border)}:host>sci-table-viewport{display:block;position:relative;height:100%;overflow:auto;scrollbar-width:none;isolation:isolate;box-sizing:border-box;anchor-name:--sci-table-viewport}:host>sci-table-viewport:is(:not(:hover),:has(sci-table-header:hover))~sci-scrollbar{opacity:0}:host>sci-table-viewport:has(sci-table-header){--\\275sci-table-header-visible: true}:host>sci-table-viewport:has(sci-table-header>sci-column-header){--\\275sci-column-header-visible: true}:host>sci-table-viewport:has(~sci-scrollbar.scrolling){--\\275sci-table-scrolling: true}:host>sci-table-viewport>sci-table-grid{display:grid;grid-template-columns:var(--\\275sci-table-columns, repeat(auto-fit, 100px)) 0;grid-auto-rows:min-content;grid-template-rows:min-content 1fr;width:min-content;min-width:100%;min-height:100%}@container not style(--\\275sci-table-header-visible: true){:host>sci-table-viewport>sci-table-grid{grid-template-rows:1fr}}:host>sci-table-viewport>sci-table-grid>sci-table-header{z-index:1;grid-row:1;grid-column:1/-1;position:sticky;top:0}:host>sci-table-viewport>sci-table-grid>sci-table-body{anchor-name:--sci-table-body;grid-row:2}@container not style(--\\275sci-table-header-visible: true){:host>sci-table-viewport>sci-table-grid>sci-table-body{grid-row:1}}:host>sci-table-viewport>sci-table-grid>sci-table-body{display:grid;grid-template-columns:subgrid;grid-column:1/-1;grid-auto-rows:var(--sci-table-row-height);outline:none;padding-top:var(--\\275sci-table-virtual-scroll-offset-top);padding-bottom:var(--\\275sci-table-virtual-scroll-offset-bottom)}:host>sci-table-viewport>sci-table-grid>sci-table-body>sci-table-row{grid-column:1/-1}:host>sci-table-viewport>sci-table-grid>sci-table-body>sci-table-row[data-active]{anchor-name:--sci-table-active-row}:host>sci-table-viewport>sci-table-grid>sci-table-body>div.active-row-outline{position:absolute;top:anchor(--sci-table-active-row top);right:anchor(--sci-table-active-row right);bottom:anchor(--sci-table-active-row bottom);left:anchor(--sci-table-active-row left);border:var(--sci-table-row-outline-width) var(--sci-table-row-outline-style) var(--sci-table-row-outline-color);border-radius:var(--sci-table-row-border-radius);pointer-events:none}:host>sci-table-viewport>sci-table-grid>sci-table-body:not(:has(sci-table-row[data-active]))>div.active-row-outline{display:none}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message{grid-column:1/-1;grid-row:auto/span 2;justify-self:center;align-self:center;display:flex;flex-direction:column;align-items:center;gap:1em;-webkit-user-select:none;user-select:none;white-space:nowrap;color:var(--sci-color-text-subtle)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message:has(button.action){grid-row:auto/span 4}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action{all:unset;display:inline-grid;place-content:center;place-items:center;padding:.25em;border-radius:var(--sci-button-border-radius);-webkit-user-select:none;user-select:none;overflow:hidden;cursor:var(--sci-button-cursor);--sci-icon-size: var(--sci-button-icon-size)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:hover:where(:not(:disabled)){background-color:var(--sci-button-background-color-hover)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:active:where(:not(:disabled)){background-color:var(--sci-button-background-color-active)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:focus:not(:focus-visible){outline:none}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:focus-visible{outline:var(--sci-button-outline-width) solid var(--sci-color-accent)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:disabled{color:var(--sci-button-text-color-disabled)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action{display:flex;gap:.5em;border:1px solid var(--sci-color-border);padding:.35em 1em}@container style(--\\275sci-table-viewport-client-width > --\\275sci-table-viewport-width){:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message{position:sticky;left:50%;right:50%;width:0}}:host>sci-table-viewport>sci-table-grid>sci-table-body:has(>span.message){z-index:2}:host>sci-table-viewport>sci-table-grid>sci-table-columns{grid-area:1/1/-1/-1;z-index:1;position:sticky;top:0;max-height:var(--\\275sci-table-viewport-height)}:host>sci-scrollbar{position:absolute}:host>sci-scrollbar[direction=vscroll]{top:calc(anchor(--sci-table-body top) + 2px);right:calc(anchor(--sci-table-viewport right) + 2px);bottom:calc(anchor(--sci-table-viewport bottom) + 2px)}:host>sci-scrollbar[direction=hscroll]{left:calc(anchor(--sci-table-viewport left) + 2px);bottom:calc(anchor(--sci-table-viewport bottom) + 1px);right:calc(anchor(--sci-table-viewport right) + 2px)}@container style(--\\275sci-table-resizing: true){:host>sci-scrollbar{pointer-events:none}}:host>sci-throbber.loading-indicator{--sci-throbber-size: 16px;position:absolute;top:calc(anchor(--sci-table-viewport top) + .5em);right:calc(anchor(--sci-table-viewport right) + .5em)}:host>div.synthetic{position:absolute;pointer-events:none;visibility:hidden}:host>div.synthetic.item-size{height:var(--sci-table-row-height)}:host>div.synthetic.cell-padding{width:var(--sci-table-cell-padding-inline)}\n"], dependencies: [{ kind: "component", type: SciTableViewportComponent, selector: "sci-table-viewport" }, { kind: "component", type: SciTableHeaderComponent, selector: "sci-table-header" }, { kind: "component", type: SciTableGridComponent, selector: "sci-table-grid" }, { kind: "component", type: SciTableBodyComponent, selector: "sci-table-body" }, { kind: "component", type: SciTableRowComponent, selector: "sci-table-row", inputs: ["row"], outputs: ["primaryAction"] }, { kind: "component", type: SciTableColumnsComponent, selector: "sci-table-columns", inputs: ["rows"] }, { kind: "directive", type: SciTableKeyboardNavigatorDirective, selector: "[sciTableKeyboardNavigator]" }, { kind: "directive", type: SciAttributesDirective, selector: "[sciAttributes]", inputs: ["sciAttributes"] }, { kind: "component", type: SciScrollbarComponent, selector: "sci-scrollbar", inputs: ["direction", "viewport"] }, { kind: "component", type: SciThrobberComponent, selector: "sci-throbber", inputs: ["type"] }, { kind: "component", type: SciIconComponent, selector: "sci-icon" }, { kind: "pipe", type: SciTextPipe, name: "sciText" }], encapsulation: i0.ViewEncapsulation.ShadowDom });
|
|
2692
3035
|
}
|
|
2693
3036
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: SciTableComponent, decorators: [{
|
|
2694
3037
|
type: Component,
|
|
@@ -2712,7 +3055,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImpor
|
|
|
2712
3055
|
], providers: [
|
|
2713
3056
|
provideSciTable(),
|
|
2714
3057
|
SciTableSelectionService,
|
|
2715
|
-
], template: "@let table = this.table();\n\n<!-- TODO [#384] Replace with sci-viewport -->\n<sci-table-viewport #viewport>\n <sci-table-grid>\n <!-- Table header -->\n @if (table.showHeader() || table.filterable()) {\n <sci-table-header/>\n }\n\n <!-- Table body -->\n <sci-table-body sciTableKeyboardNavigator>\n @if (!table.error()) {\n <!-- Track index to reuse the table row elements -->\n @for (row of table.rows(); track $index) {\n <sci-table-row [row]=\"row\" [sciAttributes]=\"row.bindings
|
|
3058
|
+
], template: "@let table = this.table();\n\n<!-- TODO [#384] Replace with sci-viewport -->\n<sci-table-viewport #viewport>\n <sci-table-grid>\n <!-- Table header -->\n @if (table.showHeader() || table.filterable()) {\n <sci-table-header/>\n }\n\n <!-- Table body -->\n <sci-table-body sciTableKeyboardNavigator>\n @if (!table.error()) {\n <!-- Track index to reuse the table row elements -->\n @for (row of table.rows(); track $index) {\n <sci-table-row [row]=\"row\" [sciAttributes]=\"row.bindings.attributes()\" [class]=\"row.bindings.classes()\" (primaryAction)=\"onRowPrimaryAction($event)\"/>\n } @empty {\n <span class=\"message e2e-no-data\">{{('%scion:components:no_items.message' | sciText)()}}</span>\n }\n } @else {\n <span class=\"message e2e-datasource\">\n {{('%scion:components:datasource_error.message' | sciText)()}}\n\n <button (click)=\"onRetry()\" class=\"action e2e-datasource-retry\">\n <sci-icon>scion:reload</sci-icon>\n <span>{{('%scion:components:retry.action' | sciText)()}}</span>\n </button>\n </span>\n }\n\n <!-- Render active row outline separately to preserve row border-radius within block selection. -->\n @if (table.activeRow()) {\n <div class=\"active-row-outline e2e-active-row-outline\"></div>\n }\n </sci-table-body>\n\n <!-- Render column splitters. -->\n <sci-table-columns [rows]=\"rows()\"/>\n </sci-table-grid>\n</sci-table-viewport>\n\n<!-- Scrollbars -->\n<sci-scrollbar direction=\"hscroll\" [viewport]=\"viewport.host\"/>\n<sci-scrollbar direction=\"vscroll\" [viewport]=\"viewport.host\"/>\n\n@if (table.loading()) {\n <sci-throbber type=\"spinner\" class=\"loading-indicator e2e-loading-indicator\"/>\n}\n\n<!-- Synthetic element to compute the absolute pixel value of the `--sci-table-row-height`, relative to the table's font size if em-based. -->\n<div class=\"synthetic item-size\" #item_size_synthetic_element></div>\n\n<!-- Synthetic element to compute the absolute pixel value of the `--sci-table-cell-padding-inline`, relative to the table's font size if em-based. -->\n<div class=\"synthetic cell-padding\" #cell_padding_synthetic_element></div>\n", styles: ["@charset \"UTF-8\";:host{display:block;overflow:hidden;--\\275sci-table-border-size: 1px;--\\275sci-table-border-color: var(--sci-color-border)}:host>sci-table-viewport{display:block;position:relative;height:100%;overflow:auto;scrollbar-width:none;isolation:isolate;box-sizing:border-box;anchor-name:--sci-table-viewport}:host>sci-table-viewport:is(:not(:hover),:has(sci-table-header:hover))~sci-scrollbar{opacity:0}:host>sci-table-viewport:has(sci-table-header){--\\275sci-table-header-visible: true}:host>sci-table-viewport:has(sci-table-header>sci-column-header){--\\275sci-column-header-visible: true}:host>sci-table-viewport:has(~sci-scrollbar.scrolling){--\\275sci-table-scrolling: true}:host>sci-table-viewport>sci-table-grid{display:grid;grid-template-columns:var(--\\275sci-table-columns, repeat(auto-fit, 100px)) 0;grid-auto-rows:min-content;grid-template-rows:min-content 1fr;width:min-content;min-width:100%;min-height:100%}@container not style(--\\275sci-table-header-visible: true){:host>sci-table-viewport>sci-table-grid{grid-template-rows:1fr}}:host>sci-table-viewport>sci-table-grid>sci-table-header{z-index:1;grid-row:1;grid-column:1/-1;position:sticky;top:0}:host>sci-table-viewport>sci-table-grid>sci-table-body{anchor-name:--sci-table-body;grid-row:2}@container not style(--\\275sci-table-header-visible: true){:host>sci-table-viewport>sci-table-grid>sci-table-body{grid-row:1}}:host>sci-table-viewport>sci-table-grid>sci-table-body{display:grid;grid-template-columns:subgrid;grid-column:1/-1;grid-auto-rows:var(--sci-table-row-height);outline:none;padding-top:var(--\\275sci-table-virtual-scroll-offset-top);padding-bottom:var(--\\275sci-table-virtual-scroll-offset-bottom)}:host>sci-table-viewport>sci-table-grid>sci-table-body>sci-table-row{grid-column:1/-1}:host>sci-table-viewport>sci-table-grid>sci-table-body>sci-table-row[data-active]{anchor-name:--sci-table-active-row}:host>sci-table-viewport>sci-table-grid>sci-table-body>div.active-row-outline{position:absolute;top:anchor(--sci-table-active-row top);right:anchor(--sci-table-active-row right);bottom:anchor(--sci-table-active-row bottom);left:anchor(--sci-table-active-row left);border:var(--sci-table-row-outline-width) var(--sci-table-row-outline-style) var(--sci-table-row-outline-color);border-radius:var(--sci-table-row-border-radius);pointer-events:none}:host>sci-table-viewport>sci-table-grid>sci-table-body:not(:has(sci-table-row[data-active]))>div.active-row-outline{display:none}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message{grid-column:1/-1;grid-row:auto/span 2;justify-self:center;align-self:center;display:flex;flex-direction:column;align-items:center;gap:1em;-webkit-user-select:none;user-select:none;white-space:nowrap;color:var(--sci-color-text-subtle)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message:has(button.action){grid-row:auto/span 4}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action{all:unset;display:inline-grid;place-content:center;place-items:center;padding:.25em;border-radius:var(--sci-button-border-radius);-webkit-user-select:none;user-select:none;overflow:hidden;cursor:var(--sci-button-cursor);--sci-icon-size: var(--sci-button-icon-size)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:hover:where(:not(:disabled)){background-color:var(--sci-button-background-color-hover)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:active:where(:not(:disabled)){background-color:var(--sci-button-background-color-active)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:focus:not(:focus-visible){outline:none}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:focus-visible{outline:var(--sci-button-outline-width) solid var(--sci-color-accent)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action:disabled{color:var(--sci-button-text-color-disabled)}:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message>button.action{display:flex;gap:.5em;border:1px solid var(--sci-color-border);padding:.35em 1em}@container style(--\\275sci-table-viewport-client-width > --\\275sci-table-viewport-width){:host>sci-table-viewport>sci-table-grid>sci-table-body>span.message{position:sticky;left:50%;right:50%;width:0}}:host>sci-table-viewport>sci-table-grid>sci-table-body:has(>span.message){z-index:2}:host>sci-table-viewport>sci-table-grid>sci-table-columns{grid-area:1/1/-1/-1;z-index:1;position:sticky;top:0;max-height:var(--\\275sci-table-viewport-height)}:host>sci-scrollbar{position:absolute}:host>sci-scrollbar[direction=vscroll]{top:calc(anchor(--sci-table-body top) + 2px);right:calc(anchor(--sci-table-viewport right) + 2px);bottom:calc(anchor(--sci-table-viewport bottom) + 2px)}:host>sci-scrollbar[direction=hscroll]{left:calc(anchor(--sci-table-viewport left) + 2px);bottom:calc(anchor(--sci-table-viewport bottom) + 1px);right:calc(anchor(--sci-table-viewport right) + 2px)}@container style(--\\275sci-table-resizing: true){:host>sci-scrollbar{pointer-events:none}}:host>sci-throbber.loading-indicator{--sci-throbber-size: 16px;position:absolute;top:calc(anchor(--sci-table-viewport top) + .5em);right:calc(anchor(--sci-table-viewport right) + .5em)}:host>div.synthetic{position:absolute;pointer-events:none;visibility:hidden}:host>div.synthetic.item-size{height:var(--sci-table-row-height)}:host>div.synthetic.cell-padding{width:var(--sci-table-cell-padding-inline)}\n"] }]
|
|
2716
3059
|
}], ctorParameters: () => [], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], table: [{ type: i0.Input, args: [{ isSignal: true, alias: "table", required: true }] }], primaryAction: [{ type: i0.Output, args: ["primaryAction"] }], _viewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SciTableViewportComponent), { ...{ read: ElementRef }, isSignal: true }] }], _viewportClient: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SciTableGridComponent), { ...{ read: ElementRef }, isSignal: true }] }], _tableHeader: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SciTableHeaderComponent), { ...{ read: ElementRef }, isSignal: true }] }], _tableBody: [{ type: i0.ViewChild, args: [i0.forwardRef(() => SciTableBodyComponent), { ...{ read: ElementRef }, isSignal: true }] }], _itemSizeSyntheticElement: [{ type: i0.ViewChild, args: ['item_size_synthetic_element', { isSignal: true }] }], _cellPaddingSyntheticElement: [{ type: i0.ViewChild, args: ['cell_padding_synthetic_element', { isSignal: true }] }], rows: [{ type: i0.ViewChildren, args: [i0.forwardRef(() => SciTableRowComponent), { isSignal: true }] }] } });
|
|
2717
3060
|
function provideSciTable() {
|
|
2718
3061
|
return {
|
|
@@ -2735,5 +3078,5 @@ function provideSciTable() {
|
|
|
2735
3078
|
* Generated bundle index. Do not edit.
|
|
2736
3079
|
*/
|
|
2737
3080
|
|
|
2738
|
-
export { SciTableComponent,
|
|
3081
|
+
export { SciTableComponent, provideTableDatasource, provideTableRowBinding, table, ɵSciTable, ɵSciTableRowBindingFactory, ɵprovideTableDatasource };
|
|
2739
3082
|
//# sourceMappingURL=scion-components-table.mjs.map
|