@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.
@@ -1,15 +1,15 @@
1
1
  import * as i0 from '@angular/core';
2
- import { computed, inject, Injector, DEFAULT_CURRENCY_CODE, untracked, signal, InjectionToken, makeEnvironmentProviders, DestroyRef, runInInjectionContext, onIdle, Injectable, linkedSignal, isSignal, NgZone, effect, resource, assertNotInReactiveContext, assertInInjectionContext, Pipe, input, Component, output, viewChild, Directive, afterNextRender, ElementRef, debounced, viewChildren, ViewEncapsulation } from '@angular/core';
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 { coerceSignal, SCI_LOCALE, ɵSingleActiveInjectorFactory as _SingleActiveInjectorFactory, toLazyObservable, ɵcreateDestroyableInjector as _createDestroyableInjector, SciComponentOutletDirective, SciTooltipOnOverflowDirective, TOOLTIP_ON_OVERFLOW_DIRECTIVE_ADDITIONAL_TEXT, TOOLTIP_ON_OVERFLOW_DIRECTIVE_SOURCE_ELEMENT, SciAttributesDirective } from '@scion/components/common';
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 { toObservable, rxResource, takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
- import { Objects, Arrays, clamp, runSafe, Observables } from '@scion/toolkit/util';
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 on an array datasource.
243
- if (this._table.isArrayDatasource() && descriptor.filterable === true) {
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 on an array datasource.
247
- if (this._table.isArrayDatasource() && descriptor.sortable === true) {
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 on an array datasource.
265
- if (this._table.isArrayDatasource() && descriptor.filterable === true) {
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 on an array datasource.
269
- if (this._table.isArrayDatasource() && descriptor.sortable === true) {
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
- // Disallow a filter matcher if using a custom datasource.
295
- if (!this._table.isArrayDatasource() && typeof descriptor.filterable === 'object') {
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
- // Disallow a sort comparator if using a custom datasource.
299
- if (!this._table.isArrayDatasource() && typeof descriptor.sortable === 'object') {
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
- * Creates a range of integers starting at `start` and ending at `end` (including `end`).
347
- */
348
- function rangeInclusive(start, end) {
349
- const range = [];
350
- for (let i = start; i <= end; i++) {
351
- range.push(i);
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
- return range;
354
- }
355
-
356
- /*
357
- * Copyright (c) 2018-2026 Swiss Federal Railways
358
- *
359
- * This program and the accompanying materials are made
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
- value() {
627
- return this._value ??= untracked(() => this._valueFn());
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 existing = cacheCopy.get(key);
665
- if (existing) {
666
- cacheCopy.delete(key);
667
- existing.dispose();
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
- * Deletes the specified page from the cache, but only if still loading.
675
- */
676
- deleteIfLoading(key) {
677
- const cacheEntry = this._cache().get(key);
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._cache.update(cache => {
690
- for (const entry of cache.values()) {
691
- entry.dispose();
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(SCI_TABLE_STORAGE);
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
- _dataLoaderFn;
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
- filterCriteria = linkedSignal({ ...(ngDevMode ? { debugName: "filterCriteria" } : /* istanbul ignore next */ {}), source: () => this.columns(), // Discard stale criteria on column change.
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
- _selectedItems = signal(new Map(), /* @ts-ignore */
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.filterCriteria(), tableFilter: this._tableFilter() }), /* @ts-ignore */
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
- hoveredRow = computed(() => this.rowsByIndex().get(this.hoveredIndex()), /* @ts-ignore */
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
- selectedIds = computed(() => new Set([...this._selectedItems().keys()]), /* @ts-ignore */
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
- // TODO [table] Remove after datasource is final. See ɵillegaldatasource.
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
- this._dataLoaderFn = isSignal(descriptor.ɵdatasource) ? arrayDatasource(descriptor.ɵdatasource, this.columns) : descriptor.ɵdatasource;
890
- this.installCriteriaWatcher();
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 rowsByIndex = this.rowsByIndex();
923
- const rowCount = scrollRange.end - scrollRange.start;
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) => rowsByIndex.get(scrollRange.start + i) ?? createSyntheticRow(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 createSyntheticRow(index) {
1285
+ function createSkeletonRow(index) {
928
1286
  return {
929
1287
  index,
930
- loading: true,
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
- const totalCount = this.totalCount() ?? viewportRowCount; // fill viewport if no data loaded yet
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.totalCount() ?? 0;
963
- const rangeEnd = Math.min(this.scrollRange()?.end ?? 0, this.totalCount() ?? 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({ page, pageSize, sortCriteria, columnFilters, tableFilter }) {
1022
- const pageStart = page * pageSize;
1023
- const pageEnd = pageStart + pageSize;
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
- loading: this._cache.get(cacheKey).rows.isLoading,
1028
- cancel: () => this._cache.deleteIfLoading(cacheKey),
1377
+ cancel: () => this._cache.deleteIfLoading(nodeIds),
1029
1378
  };
1030
1379
  }
1031
- const cacheEntryInjector = _createDestroyableInjector({ parent: this._injector });
1032
- const cacheEntry = {
1033
- rows: runInInjectionContext(cacheEntryInjector, () => {
1034
- // Fetch data.
1035
- const tableResponse$ = runSafe(() => Observables.coerce(this._dataLoaderFn({
1036
- start: pageStart,
1037
- end: pageEnd,
1038
- pageSize,
1039
- page,
1040
- sortCriteria,
1041
- tableFilter,
1042
- columnFilters,
1043
- })), error => throwError(() => error));
1044
- const columns$ = toObservable(this.columns);
1045
- const injectorFactory = new _SingleActiveInjectorFactory();
1046
- // Create a resource to track loading and error states.
1047
- return rxResource({
1048
- stream: () => tableResponse$.pipe(combineLatestWith(columns$), map(([response, columns]) => ({
1049
- rows: runInInjectionContext(injectorFactory.create(), () => this.mapItemsToRow(response.items, columns, pageStart)),
1050
- totalCount: response.totalCount,
1051
- })), tap({
1052
- next: ({ totalCount }) => this.totalCount.set(totalCount),
1053
- error: error => {
1054
- console.error(error);
1055
- // Reset count to scroll to the top so the user sees the error.
1056
- this.totalCount.set(0);
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
- loading: cacheEntry.rows.isLoading,
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.filterCriteria.update(filterCriteria => {
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._selectedItems.update(updateFn);
1455
+ this._tableTree.updateSelectedNodes(updateFn);
1115
1456
  }
1116
1457
  /**
1117
- * Indicates whether the built-in array datasource is used.
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
- isArrayDatasource() {
1122
- return isArrayDatasource(this._dataLoaderFn);
1460
+ isInMemoryDatasource() {
1461
+ return this._datasource instanceof SciInMemoryTableDatasource;
1123
1462
  }
1124
- reset() {
1463
+ reload() {
1125
1464
  this._cache.clear();
1126
- this.totalCount.set(undefined);
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 sortCriteria = this.sortCriteria();
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
- // Fall back to load the initial page if range is empty. Otherwise, if the table grows with its content,
1146
- // no data would ever load because the scroll range remains 0.
1147
- const pages = pagesByRange(scrollRange.start, scrollRange.end, this.pageSize) ?? [0];
1148
- pages.forEach(page => {
1149
- const pageRef = this.loadPage({
1150
- pageSize: this.pageSize,
1151
- page,
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
- * Resets cache on criteria changes.
1163
- */
1164
- installCriteriaWatcher() {
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 = pageStart + i;
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
- loading: false,
1179
- active: computed(() => item === this.activeItem()),
1180
- selected: computed(() => this.selectedIds().has(id)),
1181
- hovered: computed(() => this.hoveredRow()?.index === index),
1182
- bindings: coerceTableRowBindings(this._rowBindings, item, pageStart + i),
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.${tableName}`);
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.${this.name()}`, JSON.stringify(userSettings.value())));
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
- return this._trackBy?.(item) ?? item;
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
- if (activeItem === undefined) {
1228
- return undefined;
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
- * Gets pages by range and page size, or `null` if empty. End is exclusive.
1240
- */
1241
- function pagesByRange(start, end, pageSize) {
1242
- const startPage = Math.floor(start / pageSize);
1243
- const endPage = Math.floor((end - 1) / pageSize); // `end` is exclusive, so use the last included index (`end - 1`) for page calculation.
1244
- const pages = rangeInclusive(startPage, endPage);
1245
- return pages.length ? pages : null;
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
- * TODO [table] Remove after datasource is final.
1260
- *
1261
- * @docs-private Not public API. For internal use only.
1262
- * @experimental since 22.3.0; API and behavior may change in any version without notice.
1263
- */
1264
- function ɵillegaldatasource() {
1265
- return computed(() => {
1266
- throw Error('[SciTableError] Illegal state. ɵillegaldatasource should not be used.');
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
- function table(dataOrDescriptor, columnFactoryFn, options) {
1280
- assertNotInReactiveContext(table, 'Call table in a non-reactive (non-tracking) context, such as within the untracked() function.');
1281
- const descriptor = isSignal(dataOrDescriptor) ? ({ ɵdatasource: dataOrDescriptor, datasource: ɵillegaldatasource(), columns: columnFactoryFn, injector: options?.injector }) : dataOrDescriptor;
1282
- if (!descriptor.injector) {
1283
- assertInInjectionContext(table);
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
- const injector = descriptor.injector ?? inject(Injector);
1286
- const sciTable = runInInjectionContext(injector, () => new ɵSciTable(descriptor));
1287
- injector.get(DestroyRef).onDestroy(() => sciTable.dispose());
1288
- return sciTable;
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 rowsByIndex = table.rowsByIndex();
1305
- const row = rowsByIndex.get(index);
1306
- const item = rowsByIndex.get(index)?.item;
1307
- const previousFocusedIndex = table.activeRow()?.index ?? -1;
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 Map().set(row.id, item));
1680
+ table.updateSelectedItems(() => new Set().add(rowId));
1314
1681
  return;
1315
1682
  }
1316
- if (event.shiftKey && previousFocusedIndex >= 0) {
1317
- const start = Math.min(previousFocusedIndex, index);
1318
- const end = Math.max(previousFocusedIndex, index);
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(item);
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 Map().set(row.id, item));
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 = this._table().activeRow()?.index ?? -1;
1333
- if (activeIndex <= 0) {
1334
- return;
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 = this._table().activeRow()?.index ?? -1;
1364
- const endIndex = Math.min(activeIndex + 1, table.totalCount() - 1);
1365
- if (endIndex === activeIndex) {
1366
- return;
1367
- }
1368
- // Set active item.
1369
- await this.loadMissingItems(endIndex, endIndex);
1370
- const endItem = table.rowsByIndex().get(endIndex)?.item;
1371
- table.activeItem.set(endItem);
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
- const keyboardEvent = event;
1376
- const shift = keyboardEvent.shiftKey;
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 (ctrlOrMeta) {
1385
- // Selection remains unchanged.
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
- await this.selectItems(endIndex, endIndex);
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 onPageUp(event) {
1746
+ async onArrowRight(event) {
1392
1747
  event.preventDefault();
1393
1748
  const table = this._table();
1394
- const activeIndex = this._table().activeRow()?.index ?? -1;
1395
- if (activeIndex === 0) {
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
- // Set active item.
1399
- const startIndex = Math.max(activeIndex - table.viewportPageSize() + 1, 0);
1400
- await this.loadMissingItems(startIndex, startIndex);
1401
- const startItem = table.rowsByIndex().get(startIndex)?.item;
1402
- table.activeItem.set(startItem);
1403
- if (!table.selectable()) {
1404
- return;
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 = this._table().activeRow()?.index ?? -1;
1426
- const endIndex = Math.min(activeIndex + table.viewportPageSize() - 1, table.totalCount() - 1);
1427
- if (endIndex === activeIndex) {
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
- const table = this._table();
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 activeIndex = this._table().activeRow()?.index ?? -1;
1488
- const endIndex = table.totalCount() - 1;
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 activeItem = table.activeItem();
1524
- if (!activeItem) {
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(activeItem);
1795
+ this.toggleSelectedItem(activeItemId);
1532
1796
  }
1533
1797
  else {
1534
- table.updateSelectedItems(() => new Map().set(activeItemId, activeItem));
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.totalCount() ?? 0;
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
- // TODO [mmu] Should we add overloads for all variants for better readability, but same logic?
1546
- // shiftArrowDown
1547
- // shiftPageDown
1548
- // ShiftEnd
1549
- async shiftDown(options) {
1809
+ async selectOnKeyboardNavigation(targetIndex, event) {
1550
1810
  const table = this._table();
1551
- const selectedIds = table.selectedIds();
1552
- const activeIndex = options.activeIndex;
1553
- const startIndex = activeIndex;
1554
- const endIndex = options.endIndex;
1555
- const activeItem = table.rowsByIndex().get(activeIndex).item;
1556
- const activeItemId = table.trackBy(activeItem);
1557
- if (table.selectable() === 'single') {
1558
- await this.selectItems(endIndex, endIndex);
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 (!selectedIds.has(activeItemId)) {
1562
- await this.selectItems(startIndex, endIndex);
1563
- return;
1825
+ if (shift && ctrlOrMeta) {
1826
+ await this.selectItems(startIndex, endIndex, { add: true });
1564
1827
  }
1565
- if (this.isSelectionAtBlockStart(activeIndex)) {
1566
- await this.selectFromBlockStart(startIndex, endIndex);
1567
- return;
1828
+ else if (shift) {
1829
+ await this.selectOnShiftNavigation(direction, startIndex, endIndex);
1568
1830
  }
1569
- await this.selectItems(startIndex, endIndex, { add: true });
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(startIndex, endIndex, { add: true });
1835
+ await this.selectItems(targetIndex, targetIndex);
1580
1836
  }
1837
+ table.setActiveItem(targetItem);
1581
1838
  }
1582
- async shiftUp(options) {
1839
+ selectOnShiftNavigation(direction, startIndex, endIndex) {
1583
1840
  const table = this._table();
1584
- const selectedIds = table.selectedIds();
1585
- const activeIndex = options.activeIndex;
1586
- const startIndex = options.startIndex;
1587
- const endIndex = activeIndex;
1588
- const activeItem = table.rowsByIndex().get(activeIndex).item;
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
- if (this.isSelectionAtBlockEnd(activeIndex)) {
1599
- await this.selectFromBlockEnd(startIndex, endIndex);
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
- else {
1612
- await this.selectItems(startIndex, endIndex, { add: true });
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.selectedIds();
1618
- const previousItem = table.rowsByIndex().get(activeIndex - 1)?.item;
1619
- const previousItemId = previousItem ? table.trackBy(previousItem) : undefined;
1620
- const nextItem = table.rowsByIndex().get(activeIndex + 1)?.item;
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.selectedIds();
1627
- const previousItem = table.rowsByIndex().get(activeIndex - 1)?.item;
1628
- const previousItemId = previousItem ? table.trackBy(previousItem) : undefined;
1629
- const nextItem = table.rowsByIndex().get(activeIndex + 1)?.item;
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
- let rows = indices.map(i => table.rowsByIndex().get(i));
1649
- if (rows.some(row => row?.id === undefined)) {
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?.id !== undefined && row.item !== undefined)
1661
- .map((row) => [row.id, row.item]),
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 Map(selection);
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 item = table.rowsByIndex().get(currentIndex)?.item;
1674
- if (!item) {
1896
+ const itemId = table.visibleRows()?.[currentIndex]?.id;
1897
+ if (itemId === undefined) {
1675
1898
  continue;
1676
1899
  }
1677
- const itemId = table.trackBy(item);
1678
- const nextItem = table.rowsByIndex().get(nextIndex)?.item;
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
- newSelection.set(itemId, item);
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 Map(selection);
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 item = table.rowsByIndex().get(currentIndex)?.item;
1700
- if (!item) {
1922
+ const itemId = table.visibleRows()?.[currentIndex]?.id;
1923
+ if (itemId === undefined) {
1701
1924
  continue;
1702
1925
  }
1703
- const itemId = table.trackBy(item);
1704
- const previousItem = table.rowsByIndex().get(previousIndex)?.item;
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
- newSelection.set(itemId, item);
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(item) {
1939
+ toggleSelectedItem(id) {
1717
1940
  const table = this._table();
1718
- const id = table.trackBy(item);
1719
1941
  table.updateSelectedItems(selection => {
1720
- const next = new Map(selection);
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.set(id, item);
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() ? 'row:selected' : 'row:not:selected',
2041
+ this.row().selected() ? `${partType}:selected` : `${partType}:not:selected`,
1818
2042
  // Enable row styling based on active state.
1819
- this.row().active() ? 'row:active' : 'row:not: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?.parts()),
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
- if (this.row().loading) {
2111
+ const row = this.row();
2112
+ if (row.id === skeletonRowId) {
1888
2113
  return;
1889
2114
  }
1890
- void this._selectionService.onRowClick(this.row().index, event);
2115
+ const index = this.table().rowIndexById().get(row.id) ?? -1;
2116
+ void this._selectionService.onRowClick(index, event);
1891
2117
  }
1892
2118
  onRowDblClick(event) {
1893
- if (this.row().loading) {
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: [this.row().item] });
2123
+ this.primaryAction.emit({ sourceEvent: event, items: [row.id] });
1897
2124
  }
1898
2125
  onRowMouseEnter() {
1899
- this.table().hoveredIndex.set(this.row().index);
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().activeItem.set(this.row().item);
2155
+ this.table().toggleRow(row.id);
1912
2156
  }
1913
2157
  contributeRowActions() {
1914
2158
  contributeMenu(this.rowActionsToolbarName, toolbar => {
1915
- if (!this.row().loading) {
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?.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().hoveredRow() === row;\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>sci-icon:has(~sci-table-cell:first-of-type){grid-column:1;grid-row:1;align-self:center;--sci-icon-size: 1em;margin-left:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 1.5em);width:auto;max-width:var(--sci-icon-size)}:host>sci-icon:has(~sci-table-cell:first-of-type)+sci-table-cell{grid-column:1;grid-row:1;border-left:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 1.5em) solid transparent}: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" }] });
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?.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().hoveredRow() === row;\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>sci-icon:has(~sci-table-cell:first-of-type){grid-column:1;grid-row:1;align-self:center;--sci-icon-size: 1em;margin-left:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 1.5em);width:auto;max-width:var(--sci-icon-size)}:host>sci-icon:has(~sci-table-cell:first-of-type)+sci-table-cell{grid-column:1;grid-row:1;border-left:calc(var(--\\275sci-table-cell-padding-inline) + (sibling-index() - 1) * 1.5em) solid transparent}: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"] }]
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
- * Indicates whether the given keyboard event was triggered from an input element.
1953
- */
1954
- isFromInput(event) {
1955
- if (event.target instanceof Element && event.target.closest('input, textarea, select') !== null) {
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.target instanceof HTMLElement && event.target.isContentEditable) {
1959
- return true;
2205
+ else if (event.key === '-' && event.location === 3) { // NumpadSubtract
2206
+ void this.onNumpadSubtract(event);
1960
2207
  }
1961
- return false;
1962
2208
  }
1963
- /**
1964
- * Indicates whether the given keyboard event was triggered from a button.
1965
- */
1966
- isFromButton(event) {
1967
- return event.target instanceof Element && event.target.closest('button') !== null;
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().reset();
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 activeRow = this.table().activeRow();
2667
- if (!activeRow) {
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 activeRowTop = activeRow.index * itemHeight;
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?.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 });
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?.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"] }]
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, provideTableRowBinding, provideTableStorage, table, ɵillegaldatasource };
3081
+ export { SciTableComponent, provideTableDatasource, provideTableRowBinding, table, ɵSciTable, ɵSciTableRowBindingFactory, ɵprovideTableDatasource };
2739
3082
  //# sourceMappingURL=scion-components-table.mjs.map