@testgorilla/tgo-ui 11.2.2 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fesm2022/testgorilla-tgo-ui-components-core.mjs +7 -0
- package/fesm2022/testgorilla-tgo-ui-components-core.mjs.map +1 -1
- package/fesm2022/testgorilla-tgo-ui-components-dialog.mjs +9 -3
- package/fesm2022/testgorilla-tgo-ui-components-dialog.mjs.map +1 -1
- package/fesm2022/testgorilla-tgo-ui-components-infinite-scroll-table.mjs +410 -205
- package/fesm2022/testgorilla-tgo-ui-components-infinite-scroll-table.mjs.map +1 -1
- package/mcp/catalog.json +1 -1
- package/package.json +1 -1
- package/types/testgorilla-tgo-ui-components-infinite-scroll-table.d.ts +203 -131
|
@@ -1,143 +1,155 @@
|
|
|
1
1
|
import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf, ScrollingModule } from '@angular/cdk/scrolling';
|
|
2
|
-
import { NgTemplateOutlet, CommonModule } from '@angular/common';
|
|
2
|
+
import { NgTemplateOutlet, AsyncPipe, CommonModule } from '@angular/common';
|
|
3
3
|
import * as i0 from '@angular/core';
|
|
4
|
-
import { input, booleanAttribute, output, viewChild, signal, computed, effect, untracked, isDevMode, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
5
|
-
import { Subscription } from 'rxjs';
|
|
4
|
+
import { input, booleanAttribute, numberAttribute, output, viewChild, signal, computed, effect, untracked, isDevMode, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
5
|
+
import { Subscription, animationFrameScheduler } from 'rxjs';
|
|
6
|
+
import { auditTime } from 'rxjs/operators';
|
|
6
7
|
import { CheckboxComponent } from '@testgorilla/tgo-ui/components/checkbox';
|
|
8
|
+
import { UiTranslatePipe } from '@testgorilla/tgo-ui/components/core';
|
|
7
9
|
import { SkeletonComponent } from '@testgorilla/tgo-ui/components/skeleton';
|
|
8
10
|
|
|
9
|
-
/** Default number of skeleton rows rendered for the initial (full-body) load. */
|
|
10
11
|
const DEFAULT_FIRST_LOAD_SKELETON_COUNT = 10;
|
|
11
|
-
|
|
12
|
-
const LOAD_MORE_SKELETON_COUNT = 3;
|
|
13
|
-
/** Default distance (in rows) from the end at which `loadMore` fires. */
|
|
12
|
+
const DEFAULT_LOAD_MORE_SKELETON_COUNT = 3;
|
|
14
13
|
const DEFAULT_LOAD_MORE_THRESHOLD = 5;
|
|
15
|
-
/** Fixed width (px) of the checkbox selection column. Must match `.ui-ist__cell--checkbox` in the SCSS. */
|
|
16
|
-
const CHECKBOX_COLUMN_WIDTH = 56;
|
|
17
14
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
15
|
+
* Fallback width for a column that declares none, shared with placeholder columns so both resolve to one
|
|
16
|
+
* identical width — header/body alignment depends on that.
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_COLUMN_WIDTH = '160px';
|
|
19
|
+
/** Elements owning their own click semantics: a click inside one must not also activate the row. */
|
|
20
|
+
const INTERACTIVE_TARGET_SELECTOR = 'a, button, input, select, textarea, [role="button"], [tabindex]';
|
|
21
|
+
/**
|
|
22
|
+
* A generic, virtualized table with scroll-driven infinite loading. Independent of `ui-table`: it reuses
|
|
23
|
+
* only stable leaf primitives (`ui-skeleton`) and duplicates the column/context type shapes so consumers
|
|
24
|
+
* can migrate with a selector swap.
|
|
26
25
|
*/
|
|
27
26
|
class InfiniteScrollTableComponent {
|
|
28
27
|
constructor() {
|
|
29
|
-
/** Rows loaded so far. The consumer appends pages to this array. */
|
|
30
|
-
this.data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
31
|
-
/** Column definitions. The first `isSticky` column pins left; the last `isSticky` column pins right. */
|
|
32
|
-
this.columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
33
28
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
29
|
+
* Rows loaded so far. **Must change by reference** (`[...rows, ...page]`): a signal input cannot observe
|
|
30
|
+
* `rows.push(...)`, so an in-place append renders nothing and leaves `loadMore` disarmed.
|
|
36
31
|
*/
|
|
32
|
+
this.data = input.required(...(ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []));
|
|
33
|
+
this.columns = input.required(...(ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []));
|
|
34
|
+
/** Fixed row height in px (the virtualization `itemSize`). Variable row heights are unsupported. */
|
|
37
35
|
this.rowHeight = input.required(...(ngDevMode ? [{ debugName: "rowHeight" }] : /* istanbul ignore next */ []));
|
|
38
|
-
/**
|
|
36
|
+
/** Stable row identity so recycled rows re-render correctly. Takes precedence over `rowIdKey`. */
|
|
39
37
|
this.trackBy = input(...(ngDevMode ? [undefined, { debugName: "trackBy" }] : /* istanbul ignore next */ []));
|
|
40
|
-
/**
|
|
38
|
+
/** Key on each row holding a stable id. Used when `trackBy` is not provided. */
|
|
41
39
|
this.rowIdKey = input(...(ngDevMode ? [undefined, { debugName: "rowIdKey" }] : /* istanbul ignore next */ []));
|
|
42
40
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* so the header cannot stick to an outer scrolling page. Kept so `ui-table` templates migrate
|
|
46
|
-
* unchanged. Default `true`.
|
|
41
|
+
* CDK's rendered buffers. Both default to a multiple of `rowHeight` (~5 and ~10 rows) rather than a
|
|
42
|
+
* fixed px value, which would mean a different row count at every `rowHeight`.
|
|
47
43
|
*/
|
|
48
|
-
this.
|
|
44
|
+
this.minBufferPx = input(undefined, ...(ngDevMode ? [{ debugName: "minBufferPx" }] : /* istanbul ignore next */ []));
|
|
45
|
+
this.maxBufferPx = input(undefined, ...(ngDevMode ? [{ debugName: "maxBufferPx" }] : /* istanbul ignore next */ []));
|
|
49
46
|
/** Max height of the internal scroll region (e.g. `'70vh'`). The body shrinks to fit fewer rows. */
|
|
50
47
|
this.maxHeight = input('70vh', ...(ngDevMode ? [{ debugName: "maxHeight" }] : /* istanbul ignore next */ []));
|
|
51
|
-
/**
|
|
48
|
+
/** Stretch to the parent's height instead of the content's; `maxHeight` becomes a cap, not the target. */
|
|
49
|
+
this.fillHeight = input(false, { ...(ngDevMode ? { debugName: "fillHeight" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
52
50
|
this.isCondensed = input(false, { ...(ngDevMode ? { debugName: "isCondensed" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
53
51
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* Selection stays data-driven and consumer-managed, exactly like `ui-table`. Default `false`.
|
|
52
|
+
* Prepends a left-pinned checkbox column driven by each row's `selected` property. Consumer-managed
|
|
53
|
+
* (like `ui-table`): toggling only emits `selectionChange`.
|
|
57
54
|
*/
|
|
58
55
|
this.selectable = input(false, { ...(ngDevMode ? { debugName: "selectable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
56
|
+
/**
|
|
57
|
+
* Makes rows focusable and activatable by click or Enter/Space. Off by default: a read-only table
|
|
58
|
+
* must not create a tab stop per rendered row or imply interactivity it does not have.
|
|
59
|
+
*/
|
|
60
|
+
this.rowsClickable = input(false, { ...(ngDevMode ? { debugName: "rowsClickable" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
59
61
|
/** First-load state: renders a full body of skeleton rows. */
|
|
60
62
|
this.loading = input(false, { ...(ngDevMode ? { debugName: "loading" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
61
63
|
/** Appends skeleton rows below the existing rows while the next page is fetched. */
|
|
62
64
|
this.loadingMore = input(false, { ...(ngDevMode ? { debugName: "loadingMore" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
63
|
-
/**
|
|
64
|
-
this.hasMore = input(
|
|
65
|
-
/** Number of skeleton rows for the first-load state. */
|
|
65
|
+
/** Required, not defaulted: a silent "never load" default would disable the headline behaviour. */
|
|
66
|
+
this.hasMore = input.required({ ...(ngDevMode ? { debugName: "hasMore" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
66
67
|
this.skeletonRowCount = input(DEFAULT_FIRST_LOAD_SKELETON_COUNT, ...(ngDevMode ? [{ debugName: "skeletonRowCount" }] : /* istanbul ignore next */ []));
|
|
67
|
-
/**
|
|
68
|
+
/** Number of skeleton rows appended below the loaded rows while `loadingMore` is true. */
|
|
69
|
+
this.loadMoreSkeletonCount = input(DEFAULT_LOAD_MORE_SKELETON_COUNT, ...(ngDevMode ? [{ debugName: "loadMoreSkeletonCount" }] : /* istanbul ignore next */ []));
|
|
70
|
+
/**
|
|
71
|
+
* Skeleton placeholder columns for a lazily-loaded column set. They deliberately sit OUTSIDE `columns`,
|
|
72
|
+
* so they cannot change which column is last and unpin a sticky last column.
|
|
73
|
+
*/
|
|
74
|
+
this.loadingColumnCount = input(0, { ...(ngDevMode ? { debugName: "loadingColumnCount" } : /* istanbul ignore next */ {}), transform: numberAttribute });
|
|
68
75
|
this.loadMoreThreshold = input(DEFAULT_LOAD_MORE_THRESHOLD, ...(ngDevMode ? [{ debugName: "loadMoreThreshold" }] : /* istanbul ignore next */ []));
|
|
69
|
-
/**
|
|
76
|
+
/** Shown when `data` is empty and not loading. */
|
|
70
77
|
this.noDataTemplate = input(...(ngDevMode ? [undefined, { debugName: "noDataTemplate" }] : /* istanbul ignore next */ []));
|
|
71
|
-
/**
|
|
78
|
+
/** Shown when `error` is `true`. Receives `{ $implicit: retry, retry }` as context. */
|
|
72
79
|
this.errorTemplate = input(...(ngDevMode ? [undefined, { debugName: "errorTemplate" }] : /* istanbul ignore next */ []));
|
|
73
|
-
/** Whether a page fetch failed. */
|
|
74
80
|
this.error = input(false, { ...(ngDevMode ? { debugName: "error" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
75
81
|
/**
|
|
76
|
-
*
|
|
77
|
-
*
|
|
82
|
+
* Shown once `hasMore` goes `false` with rows loaded. Deliberately has NO built-in fallback (unlike
|
|
83
|
+
* `noDataTemplate` / `errorTemplate`): a single-page list would otherwise flash "no more results".
|
|
78
84
|
*/
|
|
85
|
+
this.endOfListTemplate = input(...(ngDevMode ? [undefined, { debugName: "endOfListTemplate" }] : /* istanbul ignore next */ []));
|
|
86
|
+
/** Guarded: only when `hasMore && !loading && !loadingMore`, and only once per approach to the end. */
|
|
79
87
|
this.loadMore = output();
|
|
80
|
-
/** Fires from the error template's retry affordance. */
|
|
81
88
|
this.retry = output();
|
|
82
|
-
/** Fires when a data row is clicked; payload is the row plus its data index. */
|
|
83
89
|
this.rowClick = output();
|
|
84
|
-
/**
|
|
85
|
-
* Fires when a row's selection checkbox is toggled (only when `selectable`). Payload is the row,
|
|
86
|
-
* its data index, and the new selected state. The consumer updates its data's `selected` property.
|
|
87
|
-
*/
|
|
90
|
+
/** The consumer is expected to apply `selected` back onto its own data. */
|
|
88
91
|
this.selectionChange = output();
|
|
89
|
-
/** Fixed width of the checkbox column; exposed so the template can offset a sticky first column. */
|
|
90
|
-
this.checkboxColumnWidth = CHECKBOX_COLUMN_WIDTH;
|
|
91
92
|
this.viewport = viewChild(CdkVirtualScrollViewport, ...(ngDevMode ? [{ debugName: "viewport" }] : /* istanbul ignore next */ []));
|
|
92
93
|
this.headerScroll = viewChild('headerScroll', ...(ngDevMode ? [{ debugName: "headerScroll" }] : /* istanbul ignore next */ []));
|
|
93
|
-
/** Horizontal-scroll state, mirroring the existing table's `scrollSettings`. */
|
|
94
94
|
this.scrollStart = signal(true, ...(ngDevMode ? [{ debugName: "scrollStart" }] : /* istanbul ignore next */ []));
|
|
95
95
|
this.scrollEnd = signal(false, ...(ngDevMode ? [{ debugName: "scrollEnd" }] : /* istanbul ignore next */ []));
|
|
96
96
|
this.horizontalScroll = signal(false, ...(ngDevMode ? [{ debugName: "horizontalScroll" }] : /* istanbul ignore next */ []));
|
|
97
|
-
/**
|
|
98
|
-
* Measured width (px) of the body viewport's vertical scrollbar. Reserved as right padding on the
|
|
99
|
-
* header so the header's content width exactly equals the body's, keeping every column — including
|
|
100
|
-
* the last — aligned between header and body regardless of browser/scrollbar behaviour.
|
|
101
|
-
*/
|
|
97
|
+
/** Reserved as right padding on the header, so its content width exactly equals the body's. */
|
|
102
98
|
this.scrollbarWidth = signal(0, ...(ngDevMode ? [{ debugName: "scrollbarWidth" }] : /* istanbul ignore next */ []));
|
|
99
|
+
/** Added to the viewport height so on a short list the scrollbar doesn't eat into the rows. */
|
|
100
|
+
this.hScrollbarHeight = signal(0, ...(ngDevMode ? [{ debugName: "hScrollbarHeight" }] : /* istanbul ignore next */ []));
|
|
103
101
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
102
|
+
* Paging latch. Three fields, one rule: `loadMore` emits at most once per page that lands while the
|
|
103
|
+
* user is inside the trigger zone. `isLoadMoreArmed` is the latch itself, written here and by
|
|
104
|
+
* `checkLoadMore`; `dataLengthAtEmit` records how long `data` was when the last emission went out, so
|
|
105
|
+
* "the requested page arrived" is a length comparison rather than a guess about scroll positions;
|
|
106
|
+
* `firstVisibleIndex` is where the user actually is.
|
|
107
|
+
*
|
|
108
|
+
* The invariant that matters: arming and firing must read the SAME trigger zone. They used to derive it
|
|
109
|
+
* from two different thresholds, which opened bands the latch could never leave.
|
|
107
110
|
*/
|
|
108
|
-
this.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
+
this.isLoadMoreArmed = true;
|
|
112
|
+
this.dataLengthAtEmit = 0;
|
|
113
|
+
/** In-place-mutation detector; see the effect that reads these for the discriminator. */
|
|
114
|
+
this.wasLoadingMore = false;
|
|
115
|
+
this.lastDataLength = 0;
|
|
116
|
+
/** Latches once any non-empty `data` is seen, so only the very first render is exempt from resetting. */
|
|
117
|
+
this.hasSeenHead = false;
|
|
118
|
+
/** Previous `loading` value, for spotting the false -> true edge that starts a reset. */
|
|
119
|
+
this.wasLoading = false;
|
|
111
120
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* In that case the JS `scrollLeft` sync is skipped so the offset isn't applied twice; it stays as
|
|
115
|
-
* the fallback for browsers without scroll-driven animations.
|
|
121
|
+
* First visible row index, kept current from `viewport.scrolledIndexChange` (no layout read) rather
|
|
122
|
+
* than `viewport.getRenderedRange().start`, which includes CDK's read-ahead buffer.
|
|
116
123
|
*/
|
|
117
|
-
this.
|
|
118
|
-
CSS !== null &&
|
|
119
|
-
typeof CSS.supports === 'function' &&
|
|
120
|
-
CSS.supports('animation-timeline: scroll()');
|
|
124
|
+
this.firstVisibleIndex = 0;
|
|
121
125
|
this.skeletonTheme = {
|
|
122
|
-
background: '
|
|
126
|
+
background: 'var(--ui-ist-skeleton-bg)',
|
|
123
127
|
'border-radius': '4px',
|
|
124
128
|
height: '24px',
|
|
125
129
|
'margin-bottom': '0',
|
|
126
130
|
};
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// the
|
|
130
|
-
this.
|
|
131
|
+
// Pinning is positional by contract: only the FIRST column pins left, only the LAST pins right.
|
|
132
|
+
// `isSticky` elsewhere is ignored (with a dev warning), not applied as `left: 0` — that would overlay
|
|
133
|
+
// the columns before it.
|
|
134
|
+
this.stickyStartKey = computed(() => {
|
|
131
135
|
const columns = this.columns();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
this.firstStickyKey = computed(() => {
|
|
136
|
+
return columns[0]?.isSticky ? columns[0].key : undefined;
|
|
137
|
+
}, ...(ngDevMode ? [{ debugName: "stickyStartKey" }] : /* istanbul ignore next */ []));
|
|
138
|
+
this.stickyEndKey = computed(() => {
|
|
136
139
|
const columns = this.columns();
|
|
137
140
|
const lastIndex = columns.length - 1;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
// `lastIndex > 0` so a single sticky column pins to the start, not the end.
|
|
142
|
+
return lastIndex > 0 && columns[lastIndex]?.isSticky ? columns[lastIndex].key : undefined;
|
|
143
|
+
}, ...(ngDevMode ? [{ debugName: "stickyEndKey" }] : /* istanbul ignore next */ []));
|
|
144
|
+
this.defaultColumnWidth = DEFAULT_COLUMN_WIDTH;
|
|
145
|
+
this.effectiveMinBufferPx = computed(() => this.minBufferPx() ?? this.rowHeight() * 5, ...(ngDevMode ? [{ debugName: "effectiveMinBufferPx" }] : /* istanbul ignore next */ []));
|
|
146
|
+
/**
|
|
147
|
+
* The `Math.max` is load-bearing: CDK throws in dev mode when `maxBufferPx < minBufferPx`, which a
|
|
148
|
+
* consumer triggers just by raising `minBufferPx` above the derived default.
|
|
149
|
+
*/
|
|
150
|
+
this.effectiveMaxBufferPx = computed(() => Math.max(this.maxBufferPx() ?? this.rowHeight() * 10, this.effectiveMinBufferPx()), ...(ngDevMode ? [{ debugName: "effectiveMaxBufferPx" }] : /* istanbul ignore next */ []));
|
|
151
|
+
/** Stable keys for the placeholder columns, so `@for` can track them across renders. */
|
|
152
|
+
this.loadingColumns = computed(() => Array.from({ length: Math.max(0, this.loadingColumnCount()) }, (_, index) => `loading-column-${index}`), ...(ngDevMode ? [{ debugName: "loadingColumns" }] : /* istanbul ignore next */ []));
|
|
141
153
|
this.renderRows = computed(() => {
|
|
142
154
|
if (this.loading()) {
|
|
143
155
|
return Array.from({ length: Math.max(1, this.skeletonRowCount()) }, (_, i) => ({
|
|
@@ -147,89 +159,111 @@ class InfiniteScrollTableComponent {
|
|
|
147
159
|
}
|
|
148
160
|
const rows = this.data().map((item, index) => ({ kind: 'data', item, index }));
|
|
149
161
|
if (this.loadingMore() && this.hasMore()) {
|
|
150
|
-
for (let i = 0; i <
|
|
162
|
+
for (let i = 0; i < Math.max(1, this.loadMoreSkeletonCount()); i++) {
|
|
151
163
|
rows.push({ kind: 'skeleton', id: `skeleton-more-${i}` });
|
|
152
164
|
}
|
|
153
165
|
}
|
|
154
166
|
return rows;
|
|
155
167
|
}, ...(ngDevMode ? [{ debugName: "renderRows" }] : /* istanbul ignore next */ []));
|
|
156
168
|
this.hasData = computed(() => this.data().length > 0, ...(ngDevMode ? [{ debugName: "hasData" }] : /* istanbul ignore next */ []));
|
|
157
|
-
/** The virtualized body is shown for the first-load skeletons and whenever there are rows. */
|
|
158
169
|
this.showViewport = computed(() => this.loading() || this.hasData(), ...(ngDevMode ? [{ debugName: "showViewport" }] : /* istanbul ignore next */ []));
|
|
159
|
-
|
|
170
|
+
/**
|
|
171
|
+
* `loadingMore` counts as loading here too: a consumer that retries a failed first page through the same
|
|
172
|
+
* flag it uses for every page would otherwise render "no rows" with `aria-busy` set and the live region
|
|
173
|
+
* announcing a fetch.
|
|
174
|
+
*/
|
|
175
|
+
this.showEmptyState = computed(() => !this.loading() && !this.loadingMore() && !this.error() && !this.hasData(), ...(ngDevMode ? [{ debugName: "showEmptyState" }] : /* istanbul ignore next */ []));
|
|
160
176
|
this.showErrorState = computed(() => !this.loading() && this.error() && !this.hasData(), ...(ngDevMode ? [{ debugName: "showErrorState" }] : /* istanbul ignore next */ []));
|
|
161
177
|
this.showErrorFooter = computed(() => !this.loading() && this.error() && this.hasData(), ...(ngDevMode ? [{ debugName: "showErrorFooter" }] : /* istanbul ignore next */ []));
|
|
162
178
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
179
|
+
* The negative clauses keep this out of slots other states own, and `!loadingMore()` closes a one-tick
|
|
180
|
+
* flash: a fetch completing and the consumer flipping `hasMore` need not land in the same cycle.
|
|
181
|
+
*/
|
|
182
|
+
this.showEndOfList = computed(() => this.hasData() && !this.hasMore() && !this.loading() && !this.loadingMore() && !this.error(), ...(ngDevMode ? [{ debugName: "showEndOfList" }] : /* istanbul ignore next */ []));
|
|
183
|
+
/**
|
|
184
|
+
* CSS `min()` because the cap mixes units. The scrollbar reserve stops a short list from growing a
|
|
185
|
+
* spurious vertical scrollbar; fill mode returns `null` so flex owns the sizing and the reserve is moot.
|
|
167
186
|
*/
|
|
168
187
|
this.viewportHeight = computed(() => {
|
|
188
|
+
if (this.fillHeight()) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
169
191
|
const contentHeight = this.renderRows().length * this.rowHeight();
|
|
170
192
|
const scrollbarReserve = this.horizontalScroll() ? this.hScrollbarHeight() : 0;
|
|
171
193
|
return `min(${contentHeight + scrollbarReserve}px, ${this.maxHeight()})`;
|
|
172
194
|
}, ...(ngDevMode ? [{ debugName: "viewportHeight" }] : /* istanbul ignore next */ []));
|
|
173
|
-
|
|
174
|
-
|
|
195
|
+
this.translationContext = 'INFINITE_SCROLL_TABLE.';
|
|
196
|
+
/** A translation key, resolved in the template: screen-reader users are not all English speakers. */
|
|
197
|
+
this.statusMessageKey = computed(() => {
|
|
175
198
|
if (this.loading()) {
|
|
176
|
-
return
|
|
199
|
+
return `${this.translationContext}LOADING`;
|
|
177
200
|
}
|
|
178
201
|
if (this.loadingMore()) {
|
|
179
|
-
return
|
|
202
|
+
return `${this.translationContext}LOADING_MORE`;
|
|
180
203
|
}
|
|
181
204
|
return '';
|
|
182
|
-
}, ...(ngDevMode ? [{ debugName: "
|
|
205
|
+
}, ...(ngDevMode ? [{ debugName: "statusMessageKey" }] : /* istanbul ignore next */ []));
|
|
183
206
|
this.errorContext = {
|
|
184
|
-
$implicit: () => this.
|
|
185
|
-
retry: () => this.
|
|
207
|
+
$implicit: () => this.emitRetry(),
|
|
208
|
+
retry: () => this.emitRetry(),
|
|
186
209
|
};
|
|
187
210
|
this.renderRowTrackBy = (index, row) => {
|
|
188
211
|
if (row.kind === 'skeleton') {
|
|
189
212
|
return row.id;
|
|
190
213
|
}
|
|
191
|
-
|
|
192
|
-
if (explicitTrackBy) {
|
|
193
|
-
return explicitTrackBy(row.index, row.item);
|
|
194
|
-
}
|
|
195
|
-
const key = this.rowIdKey();
|
|
196
|
-
return key ? row.item[key] : row.index;
|
|
214
|
+
return this.rowIdentity(row.item, row.index);
|
|
197
215
|
};
|
|
198
|
-
// (Re-)
|
|
199
|
-
//
|
|
200
|
-
// checkLoadMore), so it runs inside `untracked()` to avoid re-running — and tearing down the
|
|
201
|
-
// subscriptions + ResizeObserver — on every data change or loading toggle.
|
|
216
|
+
// (Re-)attaches scroll listeners as the viewport enters or leaves the DOM. The setup reads other
|
|
217
|
+
// signals, so it is `untracked()` — otherwise every data change would tear the listeners down.
|
|
202
218
|
effect(onCleanup => {
|
|
203
219
|
const viewport = this.viewport();
|
|
204
220
|
if (!viewport) {
|
|
205
221
|
return;
|
|
206
222
|
}
|
|
207
223
|
untracked(() => {
|
|
224
|
+
// A freshly created CdkVirtualScrollViewport always mounts scrolled to the top, and this field
|
|
225
|
+
// carries across viewport instances (`@if (showViewport())` destroys and recreates one when `data`
|
|
226
|
+
// empties and repopulates). Leaving it stale would let `checkLoadMore` fire off a scroll position
|
|
227
|
+
// belonging to the OLD viewport.
|
|
228
|
+
this.firstVisibleIndex = 0;
|
|
208
229
|
const element = viewport.elementRef.nativeElement;
|
|
209
|
-
// CDK
|
|
210
|
-
//
|
|
211
|
-
//
|
|
230
|
+
// Both patch CDK divs that would otherwise break the ARIA table structure: its content wrapper
|
|
231
|
+
// sits between `role="rowgroup"` and the rows, its spacer is an unlabeled child of the rowgroup.
|
|
232
|
+
// Both selectors are CDK internals, so the optional chaining would swallow a CDK restructure
|
|
233
|
+
// silently. The spec asserts both attributes after mount, which is what turns such a change into a
|
|
234
|
+
// failing test rather than a quietly invalid accessibility tree.
|
|
212
235
|
element.querySelector('.cdk-virtual-scroll-content-wrapper')?.setAttribute('role', 'presentation');
|
|
236
|
+
element.querySelector('.cdk-virtual-scroll-spacer')?.setAttribute('aria-hidden', 'true');
|
|
213
237
|
const subscription = new Subscription();
|
|
214
|
-
|
|
238
|
+
// Audited to one animation frame, matching CDK's own scroll handler, so `measureScrollOffset`'s
|
|
239
|
+
// forced reflow can't run several times per frame and interleave with CDK's DOM writes.
|
|
240
|
+
subscription.add(viewport
|
|
241
|
+
.elementScrolled()
|
|
242
|
+
.pipe(auditTime(0, animationFrameScheduler))
|
|
243
|
+
.subscribe(() => this.syncHorizontalScroll()));
|
|
244
|
+
// Neither trigger is redundant: an append changes the rendered range without a scroll, and a scroll
|
|
245
|
+
// inside the read-ahead buffer moves `firstVisibleIndex` without changing the range.
|
|
215
246
|
subscription.add(viewport.renderedRangeStream.subscribe(() => this.checkLoadMore()));
|
|
216
|
-
|
|
217
|
-
|
|
247
|
+
subscription.add(viewport.scrolledIndexChange.subscribe(index => {
|
|
248
|
+
this.firstVisibleIndex = index;
|
|
249
|
+
this.checkLoadMore();
|
|
250
|
+
}));
|
|
251
|
+
// `offsetWidth - clientWidth` also counts borders: pure scrollbar only because `.ui-ist__body`
|
|
252
|
+
// has none, and it must stay borderless.
|
|
218
253
|
const measureScrollbar = () => {
|
|
219
254
|
this.scrollbarWidth.set(element.offsetWidth - element.clientWidth);
|
|
220
255
|
this.hScrollbarHeight.set(element.offsetHeight - element.clientHeight);
|
|
221
|
-
this.updateHeaderScrollMetric();
|
|
222
256
|
};
|
|
223
257
|
subscription.add(viewport.renderedRangeStream.subscribe(() => measureScrollbar()));
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
// CDK caches its measured size, so re-measure the virtual viewport on any box change or it
|
|
227
|
-
// under-renders (empty tail band). The signal-driven height changes are covered by the
|
|
228
|
-
// dedicated effect below; this catches the rest.
|
|
258
|
+
// Covers box changes no signal captures: a flex parent resizing, or a relative `maxHeight`
|
|
259
|
+
// reacting to its container. Signal-driven height changes are the effect below.
|
|
229
260
|
let resizeObserver;
|
|
230
261
|
if (typeof ResizeObserver !== 'undefined') {
|
|
231
262
|
resizeObserver = new ResizeObserver(() => {
|
|
232
263
|
measureScrollbar();
|
|
264
|
+
// A resize can cross into or out of horizontal overflow without a scroll event, leaving the
|
|
265
|
+
// sticky shadows and `viewportHeight()`'s scrollbar reserve stale.
|
|
266
|
+
this.syncHorizontalScroll();
|
|
233
267
|
this.scheduleViewportRemeasure();
|
|
234
268
|
});
|
|
235
269
|
resizeObserver.observe(element);
|
|
@@ -237,9 +271,8 @@ class InfiniteScrollTableComponent {
|
|
|
237
271
|
measureScrollbar();
|
|
238
272
|
this.syncHorizontalScroll();
|
|
239
273
|
this.checkLoadMore();
|
|
240
|
-
// The
|
|
241
|
-
//
|
|
242
|
-
// once layout is settled. Guarded because requestAnimationFrame is browser-only (SSR).
|
|
274
|
+
// The sync above runs before CDK lays out its rows, so the overflow measurement is stale until
|
|
275
|
+
// the next frame. Guarded because requestAnimationFrame is browser-only (SSR).
|
|
243
276
|
let scrollSyncFrame;
|
|
244
277
|
if (typeof requestAnimationFrame !== 'undefined') {
|
|
245
278
|
scrollSyncFrame = requestAnimationFrame(() => this.syncHorizontalScroll());
|
|
@@ -247,8 +280,7 @@ class InfiniteScrollTableComponent {
|
|
|
247
280
|
onCleanup(() => {
|
|
248
281
|
subscription.unsubscribe();
|
|
249
282
|
resizeObserver?.disconnect();
|
|
250
|
-
//
|
|
251
|
-
// (e.g. flipping to the empty/error state in the same tick).
|
|
283
|
+
// Pending frames must not fire after the viewport leaves the DOM.
|
|
252
284
|
if (scrollSyncFrame !== undefined) {
|
|
253
285
|
cancelAnimationFrame(scrollSyncFrame);
|
|
254
286
|
}
|
|
@@ -259,31 +291,31 @@ class InfiniteScrollTableComponent {
|
|
|
259
291
|
});
|
|
260
292
|
});
|
|
261
293
|
});
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// keeps the
|
|
294
|
+
// CDK caches its measured size and re-measures only on scroll or window resize, so any signal-driven
|
|
295
|
+
// height change here (`fillHeight` included: it swaps the `min()` cap for a flex-driven height) would
|
|
296
|
+
// leave the new space empty with the tail rows unrendered. Read individually rather than via
|
|
297
|
+
// `viewportHeight()`, to keep the dependency off horizontal-scroll state. `loadingColumnCount` changes
|
|
298
|
+
// content WIDTH not the box, so the ResizeObserver misses it and only the deferred
|
|
299
|
+
// `syncHorizontalScroll()` keeps the horizontal-overflow state current.
|
|
268
300
|
effect(() => {
|
|
269
301
|
this.renderRows();
|
|
270
302
|
this.rowHeight();
|
|
271
303
|
this.maxHeight();
|
|
272
304
|
this.columns();
|
|
305
|
+
this.fillHeight();
|
|
306
|
+
this.loadingColumnCount();
|
|
307
|
+
// Adds a 56px checkbox column: content width changes, the box does not.
|
|
308
|
+
this.selectable();
|
|
273
309
|
this.scheduleViewportRemeasure();
|
|
274
310
|
});
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
// NOT `loadingMore()`: a failed fetch clears `loadingMore` without changing `data`, so the guard
|
|
278
|
-
// stays disarmed and never re-fires `loadMore` in a loop (the `error()` guard is a second layer).
|
|
311
|
+
// A `data` change is the one signal that a requested page landed, so it is where the latch gets its
|
|
312
|
+
// second chance. See `rearmLoadMoreIfUnreachable` for which cases that covers and why.
|
|
279
313
|
effect(() => {
|
|
280
314
|
this.data();
|
|
281
|
-
this.
|
|
315
|
+
untracked(() => this.rearmLoadMoreIfUnreachable());
|
|
282
316
|
});
|
|
283
|
-
// Dev-only:
|
|
284
|
-
//
|
|
285
|
-
// id, surface the sharp edge so selection state can't silently attach to the wrong row after a
|
|
286
|
-
// reorder/prepend.
|
|
317
|
+
// Dev-only: with selection on and no stable id, identity falls back to the data index, so selection
|
|
318
|
+
// state can silently attach to the wrong row after a reorder/prepend.
|
|
287
319
|
effect(() => {
|
|
288
320
|
if (isDevMode() && this.selectable() && !this.trackBy() && !this.rowIdKey()) {
|
|
289
321
|
console.warn('[ui-infinite-scroll-table] `selectable` is enabled without `trackBy` or `rowIdKey`. ' +
|
|
@@ -291,65 +323,172 @@ class InfiniteScrollTableComponent {
|
|
|
291
323
|
'when rows are reordered or prepended. Provide `trackBy` or `rowIdKey` for stable identity.');
|
|
292
324
|
}
|
|
293
325
|
});
|
|
326
|
+
// Dev-only: `isSticky` reads like a per-column capability, so name the columns being ignored.
|
|
327
|
+
effect(() => {
|
|
328
|
+
if (!isDevMode()) {
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const columns = this.columns();
|
|
332
|
+
const lastIndex = columns.length - 1;
|
|
333
|
+
const ignored = columns
|
|
334
|
+
.filter((column, index) => column.isSticky && index !== 0 && index !== lastIndex)
|
|
335
|
+
.map(column => column.key);
|
|
336
|
+
if (ignored.length > 0) {
|
|
337
|
+
console.warn('[ui-infinite-scroll-table] `isSticky` is only honoured on the first and last column. ' +
|
|
338
|
+
`Ignoring it on: ${ignored.join(', ')}.`);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
// Dev-only: the in-place-append tell is a fetch completing with the SAME array reference AND a GROWN
|
|
342
|
+
// length. Length is the discriminator — a failed or empty final fetch keeps the reference but not the
|
|
343
|
+
// growth, and those are legitimate outcomes that must stay silent.
|
|
344
|
+
effect(() => {
|
|
345
|
+
const loadingMore = this.loadingMore();
|
|
346
|
+
const data = this.data();
|
|
347
|
+
const completedFetch = this.wasLoadingMore && !loadingMore;
|
|
348
|
+
const sameReference = this.lastDataReference === data;
|
|
349
|
+
const grew = data.length > this.lastDataLength;
|
|
350
|
+
this.wasLoadingMore = loadingMore;
|
|
351
|
+
this.lastDataReference = data;
|
|
352
|
+
this.lastDataLength = data.length;
|
|
353
|
+
if (isDevMode() && completedFetch && sameReference && grew) {
|
|
354
|
+
console.warn('[ui-infinite-scroll-table] `loadingMore` completed without `data` changing reference. ' +
|
|
355
|
+
'Append pages with a new array (`[...rows, ...page]`) — a signal input cannot observe an ' +
|
|
356
|
+
'in-place `push`, so the new rows will not render and `loadMore` will not fire again.');
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
// Two ways a reset is recognised. Entering `loading` is the earlier and better one: the body becomes
|
|
360
|
+
// skeletons, so the scroll belongs at the top immediately, and by the time the data lands there is
|
|
361
|
+
// nothing left to jump. The head-identity check then covers a replacement swapped in WITHOUT a loading
|
|
362
|
+
// pass — that one needs `trackBy` or `rowIdKey`, since the index fallback makes the head always `0`.
|
|
363
|
+
// Caveat: a newest-first feed that PREPENDS also resets; those consumers should call `scrollToTop()`.
|
|
364
|
+
effect(onCleanup => {
|
|
365
|
+
onCleanup(() => this.cancelResetScrollFrame());
|
|
366
|
+
const data = this.data();
|
|
367
|
+
const loading = this.loading();
|
|
368
|
+
const startedLoading = loading && !this.wasLoading;
|
|
369
|
+
this.wasLoading = loading;
|
|
370
|
+
const headIdentity = data.length > 0 ? untracked(() => this.rowIdentity(data[0], 0)) : undefined;
|
|
371
|
+
const replaced = this.hasSeenHead && data.length > 0 && headIdentity !== this.previousHeadIdentity;
|
|
372
|
+
this.previousHeadIdentity = headIdentity;
|
|
373
|
+
// Latches once and never clears: a reset usually passes through empty + `loading`, which keeps the
|
|
374
|
+
// viewport (and its scroll offset) alive, so clearing here would make the repopulation look like a
|
|
375
|
+
// first render and skip the reset.
|
|
376
|
+
this.hasSeenHead = this.hasSeenHead || data.length > 0;
|
|
377
|
+
if (!startedLoading && !replaced) {
|
|
378
|
+
// A shrink that isn't recognised as a replacement still strands the index past the new end, where
|
|
379
|
+
// it reads as "at the bottom" and fires `loadMore` for a list the user hasn't seen.
|
|
380
|
+
this.firstVisibleIndex = Math.min(this.firstVisibleIndex, Math.max(0, data.length - 1));
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
// Synchronously, ahead of the deferred scroll: CDK updates its rendered range for the new length
|
|
384
|
+
// during this change detection, and that runs `checkLoadMore` while the index still points into the
|
|
385
|
+
// old, longer list.
|
|
386
|
+
this.firstVisibleIndex = 0;
|
|
387
|
+
// Deferred a frame: CDK clamps a scroll request against the size it has already measured. Tracked so
|
|
388
|
+
// it can be cancelled — a frame must not fire after the viewport leaves the DOM.
|
|
389
|
+
if (typeof requestAnimationFrame === 'undefined') {
|
|
390
|
+
untracked(() => this.scrollToTop());
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
this.cancelResetScrollFrame();
|
|
394
|
+
this.resetScrollFrame = requestAnimationFrame(() => {
|
|
395
|
+
this.resetScrollFrame = undefined;
|
|
396
|
+
untracked(() => this.scrollToTop());
|
|
397
|
+
});
|
|
398
|
+
});
|
|
294
399
|
}
|
|
295
|
-
|
|
296
|
-
|
|
400
|
+
cancelResetScrollFrame() {
|
|
401
|
+
if (this.resetScrollFrame !== undefined) {
|
|
402
|
+
cancelAnimationFrame(this.resetScrollFrame);
|
|
403
|
+
this.resetScrollFrame = undefined;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
/** No-ops only when no rows are rendered: the empty state, or an error with no rows. */
|
|
407
|
+
scrollToIndex(index, behavior = 'auto') {
|
|
408
|
+
this.viewport()?.scrollToIndex(index, behavior);
|
|
409
|
+
}
|
|
410
|
+
scrollToTop(behavior = 'auto') {
|
|
411
|
+
this.viewport()?.scrollToIndex(0, behavior);
|
|
297
412
|
}
|
|
298
413
|
/**
|
|
299
|
-
* The single
|
|
300
|
-
*
|
|
301
|
-
* `flex-grow:0; flex-shrink:0; overflow:hidden`) to the header cell and every body cell, so a column
|
|
302
|
-
* is exactly this width in both containers — content is clipped and can never widen it. This is what
|
|
303
|
-
* keeps header and body columns aligned end-to-end regardless of cell content or horizontal scroll.
|
|
414
|
+
* The single place `T` is widened to its structural contract. Every field read on a row goes through
|
|
415
|
+
* here, so the assertion exists once instead of at each call site.
|
|
304
416
|
*/
|
|
417
|
+
asRowLike(item) {
|
|
418
|
+
return item;
|
|
419
|
+
}
|
|
420
|
+
getCellValue(item, key) {
|
|
421
|
+
return this.asRowLike(item)[key];
|
|
422
|
+
}
|
|
423
|
+
/** Applied as `flex-basis`, `min-width` AND `max-width` on header and body cells alike — see the SCSS. */
|
|
305
424
|
columnWidth(column) {
|
|
306
|
-
return column.styles?.width ??
|
|
425
|
+
return column.styles?.width ?? DEFAULT_COLUMN_WIDTH;
|
|
307
426
|
}
|
|
308
|
-
|
|
309
|
-
if (row.kind !== 'data') {
|
|
427
|
+
emitRowClick(event, row) {
|
|
428
|
+
if (!this.rowsClickable() || row.kind !== 'data') {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (this.isInteractiveTarget(event) || this.hasTextSelection()) {
|
|
310
432
|
return;
|
|
311
433
|
}
|
|
312
|
-
this.rowClick.emit({
|
|
434
|
+
this.rowClick.emit({ row: row.item, index: row.index });
|
|
313
435
|
}
|
|
314
|
-
/**
|
|
315
|
-
* Whether a row should show the selected highlight. Mirrors `ui-table`: only when `selectable` is
|
|
316
|
-
* enabled and the row's data carries a truthy `selected` property. Skeleton rows are never selected.
|
|
317
|
-
*/
|
|
318
436
|
isRowSelected(row) {
|
|
319
|
-
return this.selectable() && row.kind === 'data' && Boolean(row.item.selected);
|
|
437
|
+
return this.selectable() && row.kind === 'data' && Boolean(this.asRowLike(row.item).selected);
|
|
320
438
|
}
|
|
321
|
-
|
|
322
|
-
onSelectionToggle(row, selected) {
|
|
439
|
+
emitSelectionChange(row, selected) {
|
|
323
440
|
if (row.kind !== 'data') {
|
|
324
441
|
return;
|
|
325
442
|
}
|
|
326
|
-
this.selectionChange.emit({
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
443
|
+
this.selectionChange.emit({ row: row.item, index: row.index, selected });
|
|
444
|
+
}
|
|
445
|
+
/** Reads `--ui-ist-checkbox-width` so the offset cannot drift from the checkbox column's own width. */
|
|
446
|
+
stickyStartLeft(column) {
|
|
447
|
+
if (column.key !== this.stickyStartKey()) {
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
return this.selectable() ? 'var(--ui-ist-checkbox-width)' : '0px';
|
|
331
451
|
}
|
|
332
452
|
/**
|
|
333
|
-
*
|
|
334
|
-
*
|
|
453
|
+
* Only the outermost start-pinned cell may cast the sticky shadow: the checkbox cell is pinned too, and
|
|
454
|
+
* shadowing both would drop a shadow across the column beside it.
|
|
335
455
|
*/
|
|
336
|
-
|
|
337
|
-
return
|
|
456
|
+
isStickyStartEdge(column) {
|
|
457
|
+
return column.key === this.stickyStartKey();
|
|
338
458
|
}
|
|
339
|
-
|
|
340
|
-
|
|
459
|
+
activateRow(event, row) {
|
|
460
|
+
if (!this.rowsClickable() || row.kind !== 'data') {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
if (this.isInteractiveTarget(event)) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// Only after the guards, so a non-clickable table never swallows page-scroll-by-space.
|
|
341
467
|
event.preventDefault();
|
|
342
|
-
this.
|
|
468
|
+
this.rowClick.emit({ row: row.item, index: row.index });
|
|
469
|
+
}
|
|
470
|
+
/** A clickable row matches `[tabindex]` itself, so `currentTarget` must be excluded explicitly. */
|
|
471
|
+
isInteractiveTarget(event) {
|
|
472
|
+
const target = event.target;
|
|
473
|
+
if (!(target instanceof Element)) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
const interactive = target.closest(INTERACTIVE_TARGET_SELECTOR);
|
|
477
|
+
return interactive !== null && interactive !== event.currentTarget;
|
|
478
|
+
}
|
|
479
|
+
/** Whether the user is selecting text, in which case a click is a selection gesture, not activation. */
|
|
480
|
+
hasTextSelection() {
|
|
481
|
+
const selection = typeof window !== 'undefined' ? window.getSelection() : null;
|
|
482
|
+
return Boolean(selection && !selection.isCollapsed && selection.toString().length > 0);
|
|
343
483
|
}
|
|
344
484
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* driven from the ResizeObserver; repeated triggers coalesce to a single measure. The re-measure is
|
|
348
|
-
* what makes CDK recompute the rendered range to fill a viewport whose height grew at runtime.
|
|
485
|
+
* Deferred a frame to read the settled layout and never trip "ResizeObserver loop limit exceeded";
|
|
486
|
+
* repeated triggers coalesce to one measure.
|
|
349
487
|
*/
|
|
350
488
|
scheduleViewportRemeasure() {
|
|
351
489
|
if (typeof requestAnimationFrame === 'undefined') {
|
|
352
490
|
this.viewport()?.checkViewportSize();
|
|
491
|
+
this.settleLoadMore();
|
|
353
492
|
return;
|
|
354
493
|
}
|
|
355
494
|
if (this.remeasureFrame !== undefined) {
|
|
@@ -357,26 +496,42 @@ class InfiniteScrollTableComponent {
|
|
|
357
496
|
}
|
|
358
497
|
this.remeasureFrame = requestAnimationFrame(() => {
|
|
359
498
|
this.remeasureFrame = undefined;
|
|
360
|
-
// `viewport()` is null in the empty/error states and after teardown, so this safely no-ops.
|
|
361
499
|
this.viewport()?.checkViewportSize();
|
|
362
|
-
this.
|
|
500
|
+
this.syncHorizontalScroll();
|
|
501
|
+
this.settleLoadMore();
|
|
363
502
|
});
|
|
364
503
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
504
|
+
/**
|
|
505
|
+
* The third `checkLoadMore` trigger, and the only one that survives a page landing without a rendered-range
|
|
506
|
+
* change. CDK emits a range only when the virtual-for LENGTH changes, so a page whose row count exactly
|
|
507
|
+
* replaces the load-more skeletons (`data + skeletons` before, `data` after) moves nothing CDK watches —
|
|
508
|
+
* and when the new rows add no scrollable height, no scroll event can ever arrive either. Runs after the
|
|
509
|
+
* re-measure so the zone is tested against the size CDK just settled on.
|
|
510
|
+
*/
|
|
511
|
+
settleLoadMore() {
|
|
512
|
+
this.rearmLoadMoreIfUnreachable();
|
|
513
|
+
this.checkLoadMore();
|
|
375
514
|
}
|
|
376
|
-
|
|
515
|
+
emitRetry() {
|
|
377
516
|
this.retry.emit();
|
|
378
517
|
}
|
|
379
|
-
/**
|
|
518
|
+
/**
|
|
519
|
+
* The index fallback is identity-in-name-only — it cannot tell rows apart after a reorder — which is why
|
|
520
|
+
* the automatic scroll reset cannot work without `trackBy` or `rowIdKey`.
|
|
521
|
+
*/
|
|
522
|
+
rowIdentity(item, index) {
|
|
523
|
+
const explicitTrackBy = this.trackBy();
|
|
524
|
+
if (explicitTrackBy) {
|
|
525
|
+
return explicitTrackBy(index, item);
|
|
526
|
+
}
|
|
527
|
+
const key = this.rowIdKey();
|
|
528
|
+
return key ? this.asRowLike(item)[key] : index;
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Assigning an absolute `scrollLeft` is drift-free by construction: it self-corrects every scroll event
|
|
532
|
+
* and the browser clamps when the header's maximum is momentarily smaller. A progress-normalized CSS
|
|
533
|
+
* scroll-timeline cannot guarantee that, which is why it was removed — do not reintroduce one.
|
|
534
|
+
*/
|
|
380
535
|
syncHorizontalScroll() {
|
|
381
536
|
const viewport = this.viewport();
|
|
382
537
|
if (!viewport) {
|
|
@@ -385,7 +540,8 @@ class InfiniteScrollTableComponent {
|
|
|
385
540
|
const left = viewport.measureScrollOffset('left');
|
|
386
541
|
const right = viewport.measureScrollOffset('right');
|
|
387
542
|
const header = this.headerScroll();
|
|
388
|
-
if (header
|
|
543
|
+
if (header) {
|
|
544
|
+
// Rounded: columns are pixel-identical, so a sub-pixel offset misaligns every one of them.
|
|
389
545
|
header.nativeElement.scrollLeft = Math.round(left);
|
|
390
546
|
}
|
|
391
547
|
this.scrollStart.set(left <= 1);
|
|
@@ -393,13 +549,62 @@ class InfiniteScrollTableComponent {
|
|
|
393
549
|
this.horizontalScroll.set(left + right > 1);
|
|
394
550
|
}
|
|
395
551
|
/**
|
|
396
|
-
*
|
|
397
|
-
*
|
|
552
|
+
* How many rows the viewport shows. Zero until CDK has measured, which callers must treat as
|
|
553
|
+
* "unknown" rather than "none".
|
|
554
|
+
*/
|
|
555
|
+
visibleRowCount() {
|
|
556
|
+
return Math.ceil((this.viewport()?.getViewportSize() ?? 0) / this.rowHeight());
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Rows from the end that count as "approaching it". Capped at a screenful: a threshold wider than the
|
|
560
|
+
* viewport cannot mean approaching, and uncapped it made the zone unleavable at every scroll position,
|
|
561
|
+
* which is how an oversized `loadMoreThreshold` used to walk a whole dataset unattended. Clamped at 0 so
|
|
562
|
+
* a negative value can't push the trigger point past the end and disable paging outright.
|
|
398
563
|
*/
|
|
564
|
+
triggerZoneRows(visibleRows) {
|
|
565
|
+
const threshold = Math.max(0, this.loadMoreThreshold());
|
|
566
|
+
// Capped only once the viewport is measured — before that there is no screenful to compare against,
|
|
567
|
+
// and capping at 0 would silently disable paging on the first pass.
|
|
568
|
+
return visibleRows > 0 ? Math.min(threshold, visibleRows) : threshold;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The rendered range includes CDK's read-ahead buffer, so measuring it conflates the user reaching the
|
|
572
|
+
* end with CDK merely pre-rendering it. The visible end has no such slack, so it tracks the user's
|
|
573
|
+
* actual scroll position instead.
|
|
574
|
+
*/
|
|
575
|
+
isInTriggerZone(dataLength, visibleRows) {
|
|
576
|
+
return this.firstVisibleIndex + visibleRows >= dataLength - this.triggerZoneRows(visibleRows);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Re-arms the latch when something other than scrolling has to do it. Two cases, both of which used to
|
|
580
|
+
* be terminal stalls:
|
|
581
|
+
*
|
|
582
|
+
* - **The requested page landed.** `data` grew past its length at the last emission, so that emission is
|
|
583
|
+
* spent. Without this, re-arming needed the user to scroll *out* of the zone and back — impossible when
|
|
584
|
+
* the page was smaller than the threshold, and impossible to even attempt when the new rows added no
|
|
585
|
+
* scrollable height.
|
|
586
|
+
* - **The zone can't be left.** `visibleEnd` bottoms out at `visibleRows`, so while that floor sits
|
|
587
|
+
* inside the zone no amount of scrolling re-arms anything. Also covers a not-yet-measured viewport,
|
|
588
|
+
* which must count as unleavable or the first fill never starts.
|
|
589
|
+
*
|
|
590
|
+
* Deliberately not driven by `loadingMore` or `error`: a failed fetch clears `loadingMore` without
|
|
591
|
+
* growing `data`, and must stay disarmed rather than re-fire in a loop.
|
|
592
|
+
*/
|
|
593
|
+
rearmLoadMoreIfUnreachable() {
|
|
594
|
+
const dataLength = this.data().length;
|
|
595
|
+
const visibleRows = this.visibleRowCount();
|
|
596
|
+
if (dataLength > this.dataLengthAtEmit) {
|
|
597
|
+
this.isLoadMoreArmed = true;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (visibleRows === 0 || visibleRows >= dataLength - this.triggerZoneRows(visibleRows)) {
|
|
601
|
+
this.isLoadMoreArmed = true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
/** Emits at most once per page that lands while the user is inside the trigger zone. */
|
|
399
605
|
checkLoadMore() {
|
|
400
606
|
const viewport = this.viewport();
|
|
401
|
-
// `error()`
|
|
402
|
-
// the error (e.g. via the retry affordance), instead of immediately re-firing `loadMore`.
|
|
607
|
+
// `error()` pauses auto-loading until the consumer clears it, rather than instantly re-firing.
|
|
403
608
|
if (!viewport || !this.hasMore() || this.loading() || this.loadingMore() || this.error()) {
|
|
404
609
|
return;
|
|
405
610
|
}
|
|
@@ -407,34 +612,34 @@ class InfiniteScrollTableComponent {
|
|
|
407
612
|
if (dataLength === 0) {
|
|
408
613
|
return;
|
|
409
614
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// loadMore entirely.
|
|
413
|
-
const threshold = Math.max(0, this.loadMoreThreshold());
|
|
414
|
-
const inTriggerZone = renderedEnd >= dataLength - threshold;
|
|
415
|
-
if (!inTriggerZone) {
|
|
416
|
-
this.loadMoreArmed = true;
|
|
615
|
+
if (!this.isInTriggerZone(dataLength, this.visibleRowCount())) {
|
|
616
|
+
this.isLoadMoreArmed = true;
|
|
417
617
|
return;
|
|
418
618
|
}
|
|
419
|
-
if (this.
|
|
420
|
-
this.
|
|
619
|
+
if (this.isLoadMoreArmed) {
|
|
620
|
+
this.isLoadMoreArmed = false;
|
|
621
|
+
this.dataLengthAtEmit = dataLength;
|
|
421
622
|
this.loadMore.emit();
|
|
422
623
|
}
|
|
423
624
|
}
|
|
424
625
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InfiniteScrollTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
425
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InfiniteScrollTableComponent, isStandalone: true, selector: "ui-infinite-scroll-table", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: true, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, rowIdKey: { classPropertyName: "rowIdKey", publicName: "rowIdKey", isSignal: true, isRequired: false, transformFunction: null }, stickyHeader: { classPropertyName: "stickyHeader", publicName: "stickyHeader", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, isCondensed: { classPropertyName: "isCondensed", publicName: "isCondensed", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null }, hasMore: { classPropertyName: "hasMore", publicName: "hasMore", isSignal: true, isRequired: false, transformFunction: null }, skeletonRowCount: { classPropertyName: "skeletonRowCount", publicName: "skeletonRowCount", isSignal: true, isRequired: false, transformFunction: null }, loadMoreThreshold: { classPropertyName: "loadMoreThreshold", publicName: "loadMoreThreshold", isSignal: true, isRequired: false, transformFunction: null }, noDataTemplate: { classPropertyName: "noDataTemplate", publicName: "noDataTemplate", isSignal: true, isRequired: false, transformFunction: null }, errorTemplate: { classPropertyName: "errorTemplate", publicName: "errorTemplate", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadMore: "loadMore", retry: "retry", rowClick: "rowClick", selectionChange: "selectionChange" }, viewQueries: [{ propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }, { propertyName: "headerScroll", first: true, predicate: ["headerScroll"], descendants: true, isSignal: true }], ngImport: i0, template: "<div\n class=\"ui-ist\"\n role=\"table\"\n [class.ui-ist--condensed]=\"isCondensed()\"\n [class.ui-ist--scrolled-start]=\"!scrollStart()\"\n [class.ui-ist--scrolled-end]=\"horizontalScroll() && !scrollEnd()\"\n [attr.aria-rowcount]=\"hasMore() ? -1 : data().length + 1\"\n [attr.aria-busy]=\"loading() || loadingMore()\"\n>\n <!-- Header: a separate element above the viewport. It never scrolls vertically (stays pinned).\n The outer element reserves the body's vertical-scrollbar width as right padding, so the reserved\n strip keeps the header background and bottom border; the inner element carries the horizontal\n scrollLeft synced from the body so columns stay aligned. -->\n <div\n class=\"ui-ist__header\"\n [class.ui-ist__header--sticky]=\"stickyHeader()\"\n [style.padding-right.px]=\"scrollbarWidth()\"\n role=\"presentation\"\n >\n <div #headerScroll class=\"ui-ist__header-scroll\" role=\"presentation\">\n <div class=\"ui-ist__row ui-ist__row--header\" role=\"row\" aria-rowindex=\"1\">\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n role=\"columnheader\"\n ></div>\n }\n @for (column of columns(); track column.key; let colIndex = $index) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header\"\n role=\"columnheader\"\n [class.ui-ist__cell--sticky-start]=\"column.key === firstStickyKey()\"\n [class.ui-ist__cell--sticky-end]=\"column.key === lastStickyKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left.px]=\"stickyStartOffset(column)\"\n >\n @if (column.headerCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.headerCellTemplate; context: { $implicit: column.title, column, colIndex }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ column.title }}</span>\n }\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </div>\n </div>\n\n <!-- Body: the CDK viewport owns BOTH vertical (virtualized) and horizontal scroll, so sticky\n columns pin against it and the horizontal scrollbar sits at the viewport bottom. -->\n @if (showViewport()) {\n <cdk-virtual-scroll-viewport\n class=\"ui-ist__body\"\n role=\"rowgroup\"\n [itemSize]=\"rowHeight()\"\n [style.height]=\"viewportHeight()\"\n >\n <div\n *cdkVirtualFor=\"let row of renderRows(); let rowIndex = index; trackBy: renderRowTrackBy\"\n class=\"ui-ist__row\"\n role=\"row\"\n [class.ui-ist__row--clickable]=\"row.kind === 'data'\"\n [class.ui-ist__row--selected]=\"isRowSelected(row)\"\n [attr.aria-selected]=\"isRowSelected(row) ? true : null\"\n [attr.aria-rowindex]=\"row.kind === 'data' ? row.index + 2 : null\"\n [attr.aria-hidden]=\"row.kind === 'skeleton' ? true : null\"\n [attr.tabindex]=\"row.kind === 'data' ? 0 : null\"\n [style.height.px]=\"rowHeight()\"\n (click)=\"onRowClick(row)\"\n (keydown.enter)=\"onRowActivate($event, row)\"\n (keydown.space)=\"onRowActivate($event, row)\"\n >\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n >\n @if (row.kind === 'data') {\n <ui-checkbox\n [checked]=\"isRowSelected(row)\"\n [ariaLabel]=\"'Select row ' + (row.index + 1)\"\n (changed)=\"onSelectionToggle(row, $event)\"\n ></ui-checkbox>\n }\n </div>\n }\n @for (column of columns(); track column.key) {\n <div\n class=\"ui-ist__cell\"\n role=\"cell\"\n [class.ui-ist__cell--sticky-start]=\"column.key === firstStickyKey()\"\n [class.ui-ist__cell--sticky-end]=\"column.key === lastStickyKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left.px]=\"stickyStartOffset(column)\"\n >\n @if (row.kind === 'skeleton') {\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n } @else if (column.rowCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.rowCellTemplate; context: { $implicit: row.item, rowIndex: row.index }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ getCellValue(row.item, column.key) }}</span>\n }\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </cdk-virtual-scroll-viewport>\n }\n\n <!-- Empty and error states render the consumer template in-frame below the header, mirroring how\n ui-table drops `noDataRowTpl` into the table body. Mid-scroll errors sit below the loaded rows. -->\n @if (showErrorFooter() && errorTemplate(); as errorTpl) {\n <div class=\"ui-ist__state\" role=\"alert\">\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n </div>\n }\n\n @if (showErrorState() && errorTemplate(); as errorTpl) {\n <div class=\"ui-ist__state\" role=\"alert\">\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n </div>\n }\n\n @if (showEmptyState() && noDataTemplate(); as emptyTpl) {\n <div class=\"ui-ist__state\">\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n </div>\n }\n\n <span class=\"ui-ist__sr-only\" role=\"status\" aria-live=\"polite\">{{ statusMessage() }}</span>\n</div>\n", styles: [".bg-teal-60b{background:#1c443c}.bg-teal-30b{background:#31766a}.bg-teal-default{background:#46a997}.bg-teal-30w{background:#7ec3b6}.bg-teal-60w{background:#b5ddd5}.bg-teal-secondary{background:#cbd6cb}.bg-teal-90w{background:#ecf6f5}.bg-petrol-60b{background:#102930}.bg-petrol-30b{background:#1b4754}.bg-petrol-default{background:#276678}.bg-petrol-30w{background:#6894a0}.bg-petrol-60w{background:#a9c2c9}.bg-petrol-secondary{background:#c8d7de}.bg-petrol-90w{background:#e9f0f1}.bg-error-60b{background:#513131}.bg-error-30b{background:#8e5655}.bg-error-60w{background:#e3c3c6}.bg-error-secondary{background:#f0dad9}.bg-error-default{background:#cb7b7a}.bg-warning-secondary{background:#f0d6bb}.bg-warning-default{background:#cca45f}.bg-black{background:#000}.bg-dark{background:#888}.bg-medium{background:#e0e0e0}.bg-grey{background:#ededed}.bg-light{background:#f6f6f6}.bg-white{background:#fff}.bg-box-shadow{background:#00000014}.bg-navigation-subtitle{background:#528593}.bgc-teal-60b{background-color:#1c443c}.bgc-teal-30b{background-color:#31766a}.bgc-teal-default{background-color:#46a997}.bgc-teal-30w{background-color:#7ec3b6}.bgc-teal-60w{background-color:#b5ddd5}.bgc-teal-secondary{background-color:#cbd6cb}.bgc-teal-90w{background-color:#ecf6f5}.bgc-petrol-60b{background-color:#102930}.bgc-petrol-30b{background-color:#1b4754}.bgc-petrol-default{background-color:#276678}.bgc-petrol-30w{background-color:#6894a0}.bgc-petrol-60w{background-color:#a9c2c9}.bgc-petrol-secondary{background-color:#c8d7de}.bgc-petrol-90w{background-color:#e9f0f1}.bgc-error-60b{background-color:#513131}.bgc-error-30b{background-color:#8e5655}.bgc-error-60w{background-color:#e3c3c6}.bgc-error-secondary{background-color:#f0dad9}.bgc-error-default{background-color:#cb7b7a}.bgc-warning-secondary{background-color:#f0d6bb}.bgc-warning-default{background-color:#cca45f}.bgc-black{background-color:#000}.bgc-dark{background-color:#888}.bgc-medium{background-color:#e0e0e0}.bgc-grey{background-color:#ededed}.bgc-light{background-color:#f6f6f6}.bgc-white{background-color:#fff}.bgc-box-shadow{background-color:#00000014}.bgc-navigation-subtitle{background-color:#528593}:host{display:block}.ui-ist{display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;background:#fff}.ui-ist__header{flex:0 0 auto;box-sizing:border-box;overflow:hidden;border-bottom:1px solid #e0e0e0;background:#fff}.ui-ist__header--sticky{position:sticky;top:0;z-index:3}.ui-ist__header-scroll{overflow:hidden}@supports (animation-timeline: scroll()){.ui-ist{timeline-scope:--ui-ist-header-x}.ui-ist__body{scroll-timeline:--ui-ist-header-x x}.ui-ist__row--header .ui-ist__cell--header:not(.ui-ist__cell--sticky-start):not(.ui-ist__cell--sticky-end){animation:ui-ist-header-follow linear both;animation-timeline:--ui-ist-header-x}}@keyframes ui-ist-header-follow{to{transform:translate(calc(-1 * var(--ui-ist-header-scroll-max, 0px)))}}.ui-ist__body{flex:1 1 auto;overflow:auto;scrollbar-width:auto}.ui-ist__body::-webkit-scrollbar{width:8px;height:8px}.ui-ist__body::-webkit-scrollbar-button{display:none}.ui-ist__body::-webkit-scrollbar-thumb{background-color:#00000059;border-radius:4px}.ui-ist__body::-webkit-scrollbar-track:hover{background-color:#00000026}.ui-ist__row{display:flex;width:max-content;min-width:100%;box-sizing:border-box}.ui-ist__row--header{min-height:56px}.ui-ist__row:not(.ui-ist__row--header){border-bottom:1px solid #d3d3d3}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover{background:#fff2fc}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover .ui-ist__cell{background:#fff2fc}.ui-ist__row--selected .ui-ist__cell{background:#f4f4f4}.ui-ist__cell{flex-grow:0;flex-shrink:0;box-sizing:border-box;display:flex;align-items:center;min-width:160px;padding:8px 24px;font-size:14px;line-height:20px;overflow:hidden;background:#fff}.ui-ist__cell--header{background:#fff;font-weight:700;font-size:14px;line-height:16px}.ui-ist__cell--right{justify-content:flex-end;text-align:right}.ui-ist__cell--center{justify-content:center;text-align:center}.ui-ist__cell--sticky-start{position:sticky;left:0;z-index:2}.ui-ist__cell--sticky-end{position:sticky;right:0;z-index:2;order:1}.ui-ist__cell--checkbox{flex:0 0 56px;min-width:56px;justify-content:center;padding:0}.ui-ist__cell--spacer{flex:1 1 auto;min-width:0;padding:0}.ui-ist__cell-text{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ui-ist__skeleton{display:block;width:100%}.ui-ist--scrolled-start .ui-ist__cell--sticky-start{box-shadow:24px 8px 24px #24242414}.ui-ist--scrolled-end .ui-ist__cell--sticky-end{box-shadow:0 8px 24px 4px #24242414}.ui-ist__state{padding:8px 24px;font-size:14px;line-height:20px}.ui-ist--condensed .ui-ist__cell{padding:8px 12px}.ui-ist--condensed .ui-ist__cell:first-child{padding-left:24px}.ui-ist--condensed .ui-ist__cell--sticky-end{padding-right:24px}.ui-ist--condensed .ui-ist__cell--spacer{padding:0}.ui-ist__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: SkeletonComponent, selector: "ui-skeleton", inputs: ["count", "theme", "appearance", "isAiTheme", "applicationTheme"] }, { kind: "component", type: CheckboxComponent, selector: "ui-checkbox", inputs: ["disabled", "checked", "indeterminate", "companyColor", "name", "label", "multiple", "applicationTheme", "ariaLabel", "ariaRequired", "hasError", "hideBuiltInErrors", "hideLabelInErrors", "ariaLabelledby", "ariaDescribedby", "truncateText", "alignment", "tabIndex"], outputs: ["changed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
626
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.19", type: InfiniteScrollTableComponent, isStandalone: true, selector: "ui-infinite-scroll-table", inputs: { data: { classPropertyName: "data", publicName: "data", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: true, transformFunction: null }, rowHeight: { classPropertyName: "rowHeight", publicName: "rowHeight", isSignal: true, isRequired: true, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null }, rowIdKey: { classPropertyName: "rowIdKey", publicName: "rowIdKey", isSignal: true, isRequired: false, transformFunction: null }, minBufferPx: { classPropertyName: "minBufferPx", publicName: "minBufferPx", isSignal: true, isRequired: false, transformFunction: null }, maxBufferPx: { classPropertyName: "maxBufferPx", publicName: "maxBufferPx", isSignal: true, isRequired: false, transformFunction: null }, maxHeight: { classPropertyName: "maxHeight", publicName: "maxHeight", isSignal: true, isRequired: false, transformFunction: null }, fillHeight: { classPropertyName: "fillHeight", publicName: "fillHeight", isSignal: true, isRequired: false, transformFunction: null }, isCondensed: { classPropertyName: "isCondensed", publicName: "isCondensed", isSignal: true, isRequired: false, transformFunction: null }, selectable: { classPropertyName: "selectable", publicName: "selectable", isSignal: true, isRequired: false, transformFunction: null }, rowsClickable: { classPropertyName: "rowsClickable", publicName: "rowsClickable", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, loadingMore: { classPropertyName: "loadingMore", publicName: "loadingMore", isSignal: true, isRequired: false, transformFunction: null }, hasMore: { classPropertyName: "hasMore", publicName: "hasMore", isSignal: true, isRequired: true, transformFunction: null }, skeletonRowCount: { classPropertyName: "skeletonRowCount", publicName: "skeletonRowCount", isSignal: true, isRequired: false, transformFunction: null }, loadMoreSkeletonCount: { classPropertyName: "loadMoreSkeletonCount", publicName: "loadMoreSkeletonCount", isSignal: true, isRequired: false, transformFunction: null }, loadingColumnCount: { classPropertyName: "loadingColumnCount", publicName: "loadingColumnCount", isSignal: true, isRequired: false, transformFunction: null }, loadMoreThreshold: { classPropertyName: "loadMoreThreshold", publicName: "loadMoreThreshold", isSignal: true, isRequired: false, transformFunction: null }, noDataTemplate: { classPropertyName: "noDataTemplate", publicName: "noDataTemplate", isSignal: true, isRequired: false, transformFunction: null }, errorTemplate: { classPropertyName: "errorTemplate", publicName: "errorTemplate", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, endOfListTemplate: { classPropertyName: "endOfListTemplate", publicName: "endOfListTemplate", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loadMore: "loadMore", retry: "retry", rowClick: "rowClick", selectionChange: "selectionChange" }, host: { properties: { "class.ui-ist--fill-host": "fillHeight()" } }, viewQueries: [{ propertyName: "viewport", first: true, predicate: CdkVirtualScrollViewport, descendants: true, isSignal: true }, { propertyName: "headerScroll", first: true, predicate: ["headerScroll"], descendants: true, isSignal: true }], ngImport: i0, template: "<!-- role=\"table\", not \"grid\": row access is Tab-only with no arrow-key cell traversal, so \"grid\" would\n promise 2D keyboard navigation this does not implement. Selection is conveyed by the row's checkbox,\n not `aria-selected` \u2014 which is valid only on grid/treegrid anyway. -->\n<div\n class=\"ui-ist\"\n role=\"table\"\n [class.ui-ist--condensed]=\"isCondensed()\"\n [class.ui-ist--scrolled-start]=\"!scrollStart()\"\n [class.ui-ist--scrolled-end]=\"horizontalScroll() && !scrollEnd()\"\n [class.ui-ist--fill-height]=\"fillHeight()\"\n [attr.aria-rowcount]=\"hasMore() ? -1 : data().length + 1\"\n [attr.aria-busy]=\"loading() || loadingMore()\"\n>\n <!-- Pinned header, outside the viewport so it never scrolls vertically. The outer element reserves the\n body's scrollbar width as padding (keeping the background and border across that strip); the inner\n one carries the horizontal scrollLeft synced from the body. -->\n <div class=\"ui-ist__header\" [style.padding-right.px]=\"scrollbarWidth()\" role=\"presentation\">\n <div #headerScroll class=\"ui-ist__header-scroll\" role=\"presentation\">\n <div class=\"ui-ist__row ui-ist__row--header\" role=\"row\" aria-rowindex=\"1\">\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n [class.ui-ist__cell--sticky-start-edge]=\"!stickyStartKey()\"\n role=\"columnheader\"\n ></div>\n }\n @for (column of columns(); track column.key; let colIndex = $index) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header\"\n role=\"columnheader\"\n [class.ui-ist__cell--sticky-start]=\"column.key === stickyStartKey()\"\n [class.ui-ist__cell--sticky-start-edge]=\"isStickyStartEdge(column)\"\n [class.ui-ist__cell--sticky-end]=\"column.key === stickyEndKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left]=\"stickyStartLeft(column)\"\n >\n @if (column.headerCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.headerCellTemplate; context: { $implicit: column.title, column, colIndex }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ column.title }}</span>\n }\n </div>\n }\n @for (loadingKey of loadingColumns(); track loadingKey) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--loading\"\n role=\"presentation\"\n aria-hidden=\"true\"\n [style.flex-basis]=\"defaultColumnWidth\"\n [style.min-width]=\"defaultColumnWidth\"\n [style.max-width]=\"defaultColumnWidth\"\n >\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </div>\n </div>\n\n <!-- The CDK viewport owns BOTH vertical (virtualized) and horizontal scroll, so sticky columns pin\n against it and the horizontal scrollbar sits at the viewport bottom. -->\n @if (showViewport()) {\n <cdk-virtual-scroll-viewport\n class=\"ui-ist__body\"\n role=\"rowgroup\"\n [itemSize]=\"rowHeight()\"\n [minBufferPx]=\"effectiveMinBufferPx()\"\n [maxBufferPx]=\"effectiveMaxBufferPx()\"\n [style.height]=\"viewportHeight()\"\n [style.max-height]=\"fillHeight() ? maxHeight() : null\"\n >\n <div\n *cdkVirtualFor=\"let row of renderRows(); let rowIndex = index; trackBy: renderRowTrackBy\"\n class=\"ui-ist__row\"\n role=\"row\"\n [class.ui-ist__row--clickable]=\"rowsClickable() && row.kind === 'data'\"\n [class.ui-ist__row--selected]=\"isRowSelected(row)\"\n [attr.aria-rowindex]=\"row.kind === 'data' ? row.index + 2 : null\"\n [attr.aria-hidden]=\"row.kind === 'skeleton' ? true : null\"\n [attr.tabindex]=\"rowsClickable() && row.kind === 'data' ? 0 : null\"\n [style.height.px]=\"rowHeight()\"\n (click)=\"emitRowClick($event, row)\"\n (keydown.enter)=\"activateRow($event, row)\"\n (keydown.space)=\"activateRow($event, row)\"\n >\n @if (selectable()) {\n <!-- This `stopPropagation` is what actually stops a checkbox click activating the row: it runs\n before the click reaches the row's listener, so `isInteractiveTarget` is never consulted on\n this path. That guard is a real second line of defence (`ui-checkbox` renders a focusable\n wrapper and a native input, both matching INTERACTIVE_TARGET_SELECTOR), not a substitute. -->\n <div\n class=\"ui-ist__cell ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n [class.ui-ist__cell--sticky-start-edge]=\"!stickyStartKey()\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n >\n @if (row.kind === 'data') {\n <ui-checkbox\n [checked]=\"isRowSelected(row)\"\n [ariaLabel]=\"'Select row ' + (row.index + 1)\"\n (changed)=\"emitSelectionChange(row, $event)\"\n ></ui-checkbox>\n }\n </div>\n }\n @for (column of columns(); track column.key) {\n <div\n class=\"ui-ist__cell\"\n role=\"cell\"\n [class.ui-ist__cell--sticky-start]=\"column.key === stickyStartKey()\"\n [class.ui-ist__cell--sticky-start-edge]=\"isStickyStartEdge(column)\"\n [class.ui-ist__cell--sticky-end]=\"column.key === stickyEndKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left]=\"stickyStartLeft(column)\"\n >\n @if (row.kind === 'skeleton') {\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n } @else if (column.rowCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.rowCellTemplate; context: { $implicit: row.item, rowIndex: row.index }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ getCellValue(row.item, column.key) }}</span>\n }\n </div>\n }\n @for (loadingKey of loadingColumns(); track loadingKey) {\n <div\n class=\"ui-ist__cell ui-ist__cell--loading\"\n role=\"presentation\"\n aria-hidden=\"true\"\n [style.flex-basis]=\"defaultColumnWidth\"\n [style.min-width]=\"defaultColumnWidth\"\n [style.max-width]=\"defaultColumnWidth\"\n >\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </cdk-virtual-scroll-viewport>\n }\n\n <!-- `showErrorFooter()` and `showErrorState()` differ only on `hasData()`, so they are mutually\n exclusive and safe to merge here. Both fall back to a plain-language message, so a failed fetch is\n never a silent blank box. -->\n @if (showErrorFooter() || showErrorState()) {\n <div class=\"ui-ist__state\" role=\"alert\">\n @if (errorTemplate(); as errorTpl) {\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n } @else {\n <span>{{ translationContext + 'LOAD_ERROR' | uiTranslate | async }}</span>\n }\n </div>\n }\n\n @if (showEmptyState()) {\n <div class=\"ui-ist__state\">\n @if (noDataTemplate(); as emptyTpl) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n <span>{{ translationContext + 'NO_ROWS' | uiTranslate | async }}</span>\n }\n </div>\n }\n\n <!-- Opt-in, with no fallback (see `endOfListTemplate`). Not role=\"alert\": reaching the end is not an\n alert, and `aria-rowcount` already conveys completeness to assistive tech. -->\n @if (showEndOfList() && endOfListTemplate(); as endTpl) {\n <div class=\"ui-ist__state\">\n <ng-container *ngTemplateOutlet=\"endTpl\"></ng-container>\n </div>\n }\n\n <span class=\"ui-ist__sr-only\" role=\"status\" aria-live=\"polite\">\n @if (statusMessageKey(); as statusKey) {\n {{ statusKey | uiTranslate | async }}\n }\n </span>\n</div>\n", styles: [".bg-teal-60b{background:#1c443c}.bg-teal-30b{background:#31766a}.bg-teal-default{background:#46a997}.bg-teal-30w{background:#7ec3b6}.bg-teal-60w{background:#b5ddd5}.bg-teal-secondary{background:#cbd6cb}.bg-teal-90w{background:#ecf6f5}.bg-petrol-60b{background:#102930}.bg-petrol-30b{background:#1b4754}.bg-petrol-default{background:#276678}.bg-petrol-30w{background:#6894a0}.bg-petrol-60w{background:#a9c2c9}.bg-petrol-secondary{background:#c8d7de}.bg-petrol-90w{background:#e9f0f1}.bg-error-60b{background:#513131}.bg-error-30b{background:#8e5655}.bg-error-60w{background:#e3c3c6}.bg-error-secondary{background:#f0dad9}.bg-error-default{background:#cb7b7a}.bg-warning-secondary{background:#f0d6bb}.bg-warning-default{background:#cca45f}.bg-black{background:#000}.bg-dark{background:#888}.bg-medium{background:#e0e0e0}.bg-grey{background:#ededed}.bg-light{background:#f6f6f6}.bg-white{background:#fff}.bg-box-shadow{background:#00000014}.bg-navigation-subtitle{background:#528593}.bgc-teal-60b{background-color:#1c443c}.bgc-teal-30b{background-color:#31766a}.bgc-teal-default{background-color:#46a997}.bgc-teal-30w{background-color:#7ec3b6}.bgc-teal-60w{background-color:#b5ddd5}.bgc-teal-secondary{background-color:#cbd6cb}.bgc-teal-90w{background-color:#ecf6f5}.bgc-petrol-60b{background-color:#102930}.bgc-petrol-30b{background-color:#1b4754}.bgc-petrol-default{background-color:#276678}.bgc-petrol-30w{background-color:#6894a0}.bgc-petrol-60w{background-color:#a9c2c9}.bgc-petrol-secondary{background-color:#c8d7de}.bgc-petrol-90w{background-color:#e9f0f1}.bgc-error-60b{background-color:#513131}.bgc-error-30b{background-color:#8e5655}.bgc-error-60w{background-color:#e3c3c6}.bgc-error-secondary{background-color:#f0dad9}.bgc-error-default{background-color:#cb7b7a}.bgc-warning-secondary{background-color:#f0d6bb}.bgc-warning-default{background-color:#cca45f}.bgc-black{background-color:#000}.bgc-dark{background-color:#888}.bgc-medium{background-color:#e0e0e0}.bgc-grey{background-color:#ededed}.bgc-light{background-color:#f6f6f6}.bgc-white{background-color:#fff}.bgc-box-shadow{background-color:#00000014}.bgc-navigation-subtitle{background-color:#528593}:host{display:block}.ui-ist{--ui-ist-checkbox-width: 56px;--ui-ist-skeleton-bg: #f4f4f4;display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;background:#fff}.ui-ist__header{flex:0 0 auto;box-sizing:border-box;overflow:hidden;border-bottom:1px solid #e0e0e0;background:var(--ui-ist-header-bg, #ffffff)}.ui-ist__header-scroll{overflow:hidden}.ui-ist__body{flex:1 1 auto;overflow:auto;scrollbar-width:auto}.ui-ist__body::-webkit-scrollbar{width:8px;height:8px}.ui-ist__body::-webkit-scrollbar-button{display:none}.ui-ist__body::-webkit-scrollbar-thumb{background-color:#00000059;border-radius:4px}.ui-ist__body::-webkit-scrollbar-track:hover{background-color:#00000026}.ui-ist__row{display:flex;width:max-content;min-width:100%;box-sizing:border-box}.ui-ist__row--header{min-height:56px}.ui-ist__row:not(.ui-ist__row--header){border-bottom:1px solid #d3d3d3}.ui-ist__row--clickable{cursor:pointer}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover{background:#fff2fc}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover .ui-ist__cell{background:#fff2fc}.ui-ist__row--selected .ui-ist__cell{background:#f4f4f4}.ui-ist__cell{flex-grow:0;flex-shrink:0;box-sizing:border-box;display:flex;align-items:center;padding:8px 24px;font-size:14px;line-height:20px;overflow:hidden;background:#fff}.ui-ist__cell--header{background:var(--ui-ist-header-bg, #ffffff);font-weight:700;font-size:14px;line-height:16px}.ui-ist__cell--right{justify-content:flex-end;text-align:right}.ui-ist__cell--center{justify-content:center;text-align:center}.ui-ist__cell--sticky-start{position:sticky;z-index:2}.ui-ist__cell--checkbox.ui-ist__cell--sticky-start{left:0}.ui-ist__cell--sticky-end{position:sticky;right:0;z-index:2;order:1}.ui-ist__cell--checkbox{flex:0 0 var(--ui-ist-checkbox-width);min-width:var(--ui-ist-checkbox-width);justify-content:center;padding:0}.ui-ist__cell--spacer{flex:1 1 auto;min-width:0;padding:0}.ui-ist__row--header .ui-ist__cell--spacer{background:var(--ui-ist-header-bg, #ffffff)}.ui-ist__cell-text{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ui-ist__skeleton{display:block;width:100%}.ui-ist--scrolled-start .ui-ist__cell--sticky-start-edge{box-shadow:24px 8px 24px #24242414}.ui-ist--scrolled-end .ui-ist__cell--sticky-end{box-shadow:0 8px 24px 4px #24242414}.ui-ist__state{padding:8px 24px;font-size:14px;line-height:20px}.ui-ist--condensed .ui-ist__cell{padding:8px 12px}.ui-ist--condensed .ui-ist__cell:first-child{padding-left:24px}.ui-ist--condensed .ui-ist__cell--sticky-end{padding-right:24px}.ui-ist--condensed .ui-ist__cell--spacer{padding:0}:host(.ui-ist--fill-host){height:100%}.ui-ist--fill-height{height:100%}.ui-ist--fill-height .ui-ist__body{min-height:0}.ui-ist__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: CdkVirtualScrollViewport, selector: "cdk-virtual-scroll-viewport", inputs: ["orientation", "appendOnly"], outputs: ["scrolledIndexChange"] }, { kind: "directive", type: CdkFixedSizeVirtualScroll, selector: "cdk-virtual-scroll-viewport[itemSize]", inputs: ["itemSize", "minBufferPx", "maxBufferPx"] }, { kind: "directive", type: CdkVirtualForOf, selector: "[cdkVirtualFor][cdkVirtualForOf]", inputs: ["cdkVirtualForOf", "cdkVirtualForTrackBy", "cdkVirtualForTemplate", "cdkVirtualForTemplateCacheSize"] }, { kind: "component", type: SkeletonComponent, selector: "ui-skeleton", inputs: ["count", "theme", "appearance", "isAiTheme", "applicationTheme"] }, { kind: "component", type: CheckboxComponent, selector: "ui-checkbox", inputs: ["disabled", "checked", "indeterminate", "companyColor", "name", "label", "multiple", "applicationTheme", "ariaLabel", "ariaRequired", "hasError", "hideBuiltInErrors", "hideLabelInErrors", "ariaLabelledby", "ariaDescribedby", "truncateText", "alignment", "tabIndex"], outputs: ["changed"] }, { kind: "pipe", type: AsyncPipe, name: "async" }, { kind: "pipe", type: UiTranslatePipe, name: "uiTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
426
627
|
}
|
|
427
628
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InfiniteScrollTableComponent, decorators: [{
|
|
428
629
|
type: Component,
|
|
429
|
-
args: [{ selector: 'ui-infinite-scroll-table', changeDetection: ChangeDetectionStrategy.OnPush,
|
|
630
|
+
args: [{ selector: 'ui-infinite-scroll-table', changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
631
|
+
'[class.ui-ist--fill-host]': 'fillHeight()',
|
|
632
|
+
}, imports: [
|
|
633
|
+
AsyncPipe,
|
|
430
634
|
NgTemplateOutlet,
|
|
431
635
|
CdkVirtualScrollViewport,
|
|
432
636
|
CdkFixedSizeVirtualScroll,
|
|
433
637
|
CdkVirtualForOf,
|
|
434
638
|
SkeletonComponent,
|
|
435
639
|
CheckboxComponent,
|
|
436
|
-
], template: "<div\n class=\"ui-ist\"\n role=\"table\"\n [class.ui-ist--condensed]=\"isCondensed()\"\n [class.ui-ist--scrolled-start]=\"!scrollStart()\"\n [class.ui-ist--scrolled-end]=\"horizontalScroll() && !scrollEnd()\"\n [attr.aria-rowcount]=\"hasMore() ? -1 : data().length + 1\"\n [attr.aria-busy]=\"loading() || loadingMore()\"\n>\n <!-- Header: a separate element above the viewport. It never scrolls vertically (stays pinned).\n The outer element reserves the body's vertical-scrollbar width as right padding, so the reserved\n strip keeps the header background and bottom border; the inner element carries the horizontal\n scrollLeft synced from the body so columns stay aligned. -->\n <div\n class=\"ui-ist__header\"\n [class.ui-ist__header--sticky]=\"stickyHeader()\"\n [style.padding-right.px]=\"scrollbarWidth()\"\n role=\"presentation\"\n >\n <div #headerScroll class=\"ui-ist__header-scroll\" role=\"presentation\">\n <div class=\"ui-ist__row ui-ist__row--header\" role=\"row\" aria-rowindex=\"1\">\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n role=\"columnheader\"\n ></div>\n }\n @for (column of columns(); track column.key; let colIndex = $index) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header\"\n role=\"columnheader\"\n [class.ui-ist__cell--sticky-start]=\"column.key === firstStickyKey()\"\n [class.ui-ist__cell--sticky-end]=\"column.key === lastStickyKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left.px]=\"stickyStartOffset(column)\"\n >\n @if (column.headerCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.headerCellTemplate; context: { $implicit: column.title, column, colIndex }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ column.title }}</span>\n }\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </div>\n </div>\n\n <!-- Body: the CDK viewport owns BOTH vertical (virtualized) and horizontal scroll, so sticky\n columns pin against it and the horizontal scrollbar sits at the viewport bottom. -->\n @if (showViewport()) {\n <cdk-virtual-scroll-viewport\n class=\"ui-ist__body\"\n role=\"rowgroup\"\n [itemSize]=\"rowHeight()\"\n [style.height]=\"viewportHeight()\"\n >\n <div\n *cdkVirtualFor=\"let row of renderRows(); let rowIndex = index; trackBy: renderRowTrackBy\"\n class=\"ui-ist__row\"\n role=\"row\"\n [class.ui-ist__row--clickable]=\"row.kind === 'data'\"\n [class.ui-ist__row--selected]=\"isRowSelected(row)\"\n [attr.aria-selected]=\"isRowSelected(row) ? true : null\"\n [attr.aria-rowindex]=\"row.kind === 'data' ? row.index + 2 : null\"\n [attr.aria-hidden]=\"row.kind === 'skeleton' ? true : null\"\n [attr.tabindex]=\"row.kind === 'data' ? 0 : null\"\n [style.height.px]=\"rowHeight()\"\n (click)=\"onRowClick(row)\"\n (keydown.enter)=\"onRowActivate($event, row)\"\n (keydown.space)=\"onRowActivate($event, row)\"\n >\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n >\n @if (row.kind === 'data') {\n <ui-checkbox\n [checked]=\"isRowSelected(row)\"\n [ariaLabel]=\"'Select row ' + (row.index + 1)\"\n (changed)=\"onSelectionToggle(row, $event)\"\n ></ui-checkbox>\n }\n </div>\n }\n @for (column of columns(); track column.key) {\n <div\n class=\"ui-ist__cell\"\n role=\"cell\"\n [class.ui-ist__cell--sticky-start]=\"column.key === firstStickyKey()\"\n [class.ui-ist__cell--sticky-end]=\"column.key === lastStickyKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left.px]=\"stickyStartOffset(column)\"\n >\n @if (row.kind === 'skeleton') {\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n } @else if (column.rowCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.rowCellTemplate; context: { $implicit: row.item, rowIndex: row.index }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ getCellValue(row.item, column.key) }}</span>\n }\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </cdk-virtual-scroll-viewport>\n }\n\n <!-- Empty and error states render the consumer template in-frame below the header, mirroring how\n ui-table drops `noDataRowTpl` into the table body. Mid-scroll errors sit below the loaded rows. -->\n @if (showErrorFooter() && errorTemplate(); as errorTpl) {\n <div class=\"ui-ist__state\" role=\"alert\">\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n </div>\n }\n\n @if (showErrorState() && errorTemplate(); as errorTpl) {\n <div class=\"ui-ist__state\" role=\"alert\">\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n </div>\n }\n\n @if (showEmptyState() && noDataTemplate(); as emptyTpl) {\n <div class=\"ui-ist__state\">\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n </div>\n }\n\n <span class=\"ui-ist__sr-only\" role=\"status\" aria-live=\"polite\">{{ statusMessage() }}</span>\n</div>\n", styles: [".bg-teal-60b{background:#1c443c}.bg-teal-30b{background:#31766a}.bg-teal-default{background:#46a997}.bg-teal-30w{background:#7ec3b6}.bg-teal-60w{background:#b5ddd5}.bg-teal-secondary{background:#cbd6cb}.bg-teal-90w{background:#ecf6f5}.bg-petrol-60b{background:#102930}.bg-petrol-30b{background:#1b4754}.bg-petrol-default{background:#276678}.bg-petrol-30w{background:#6894a0}.bg-petrol-60w{background:#a9c2c9}.bg-petrol-secondary{background:#c8d7de}.bg-petrol-90w{background:#e9f0f1}.bg-error-60b{background:#513131}.bg-error-30b{background:#8e5655}.bg-error-60w{background:#e3c3c6}.bg-error-secondary{background:#f0dad9}.bg-error-default{background:#cb7b7a}.bg-warning-secondary{background:#f0d6bb}.bg-warning-default{background:#cca45f}.bg-black{background:#000}.bg-dark{background:#888}.bg-medium{background:#e0e0e0}.bg-grey{background:#ededed}.bg-light{background:#f6f6f6}.bg-white{background:#fff}.bg-box-shadow{background:#00000014}.bg-navigation-subtitle{background:#528593}.bgc-teal-60b{background-color:#1c443c}.bgc-teal-30b{background-color:#31766a}.bgc-teal-default{background-color:#46a997}.bgc-teal-30w{background-color:#7ec3b6}.bgc-teal-60w{background-color:#b5ddd5}.bgc-teal-secondary{background-color:#cbd6cb}.bgc-teal-90w{background-color:#ecf6f5}.bgc-petrol-60b{background-color:#102930}.bgc-petrol-30b{background-color:#1b4754}.bgc-petrol-default{background-color:#276678}.bgc-petrol-30w{background-color:#6894a0}.bgc-petrol-60w{background-color:#a9c2c9}.bgc-petrol-secondary{background-color:#c8d7de}.bgc-petrol-90w{background-color:#e9f0f1}.bgc-error-60b{background-color:#513131}.bgc-error-30b{background-color:#8e5655}.bgc-error-60w{background-color:#e3c3c6}.bgc-error-secondary{background-color:#f0dad9}.bgc-error-default{background-color:#cb7b7a}.bgc-warning-secondary{background-color:#f0d6bb}.bgc-warning-default{background-color:#cca45f}.bgc-black{background-color:#000}.bgc-dark{background-color:#888}.bgc-medium{background-color:#e0e0e0}.bgc-grey{background-color:#ededed}.bgc-light{background-color:#f6f6f6}.bgc-white{background-color:#fff}.bgc-box-shadow{background-color:#00000014}.bgc-navigation-subtitle{background-color:#528593}:host{display:block}.ui-ist{display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;background:#fff}.ui-ist__header{flex:0 0 auto;box-sizing:border-box;overflow:hidden;border-bottom:1px solid #e0e0e0;background:#fff}.ui-ist__header--sticky{position:sticky;top:0;z-index:3}.ui-ist__header-scroll{overflow:hidden}@supports (animation-timeline: scroll()){.ui-ist{timeline-scope:--ui-ist-header-x}.ui-ist__body{scroll-timeline:--ui-ist-header-x x}.ui-ist__row--header .ui-ist__cell--header:not(.ui-ist__cell--sticky-start):not(.ui-ist__cell--sticky-end){animation:ui-ist-header-follow linear both;animation-timeline:--ui-ist-header-x}}@keyframes ui-ist-header-follow{to{transform:translate(calc(-1 * var(--ui-ist-header-scroll-max, 0px)))}}.ui-ist__body{flex:1 1 auto;overflow:auto;scrollbar-width:auto}.ui-ist__body::-webkit-scrollbar{width:8px;height:8px}.ui-ist__body::-webkit-scrollbar-button{display:none}.ui-ist__body::-webkit-scrollbar-thumb{background-color:#00000059;border-radius:4px}.ui-ist__body::-webkit-scrollbar-track:hover{background-color:#00000026}.ui-ist__row{display:flex;width:max-content;min-width:100%;box-sizing:border-box}.ui-ist__row--header{min-height:56px}.ui-ist__row:not(.ui-ist__row--header){border-bottom:1px solid #d3d3d3}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover{background:#fff2fc}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover .ui-ist__cell{background:#fff2fc}.ui-ist__row--selected .ui-ist__cell{background:#f4f4f4}.ui-ist__cell{flex-grow:0;flex-shrink:0;box-sizing:border-box;display:flex;align-items:center;min-width:160px;padding:8px 24px;font-size:14px;line-height:20px;overflow:hidden;background:#fff}.ui-ist__cell--header{background:#fff;font-weight:700;font-size:14px;line-height:16px}.ui-ist__cell--right{justify-content:flex-end;text-align:right}.ui-ist__cell--center{justify-content:center;text-align:center}.ui-ist__cell--sticky-start{position:sticky;left:0;z-index:2}.ui-ist__cell--sticky-end{position:sticky;right:0;z-index:2;order:1}.ui-ist__cell--checkbox{flex:0 0 56px;min-width:56px;justify-content:center;padding:0}.ui-ist__cell--spacer{flex:1 1 auto;min-width:0;padding:0}.ui-ist__cell-text{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ui-ist__skeleton{display:block;width:100%}.ui-ist--scrolled-start .ui-ist__cell--sticky-start{box-shadow:24px 8px 24px #24242414}.ui-ist--scrolled-end .ui-ist__cell--sticky-end{box-shadow:0 8px 24px 4px #24242414}.ui-ist__state{padding:8px 24px;font-size:14px;line-height:20px}.ui-ist--condensed .ui-ist__cell{padding:8px 12px}.ui-ist--condensed .ui-ist__cell:first-child{padding-left:24px}.ui-ist--condensed .ui-ist__cell--sticky-end{padding-right:24px}.ui-ist--condensed .ui-ist__cell--spacer{padding:0}.ui-ist__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
437
|
-
}], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: true }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], rowIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowIdKey", required: false }] }], stickyHeader: [{ type: i0.Input, args: [{ isSignal: true, alias: "stickyHeader", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], isCondensed: [{ type: i0.Input, args: [{ isSignal: true, alias: "isCondensed", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMore", required: false }] }], hasMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasMore", required: false }] }], skeletonRowCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonRowCount", required: false }] }], loadMoreThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadMoreThreshold", required: false }] }], noDataTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDataTemplate", required: false }] }], errorTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTemplate", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], retry: [{ type: i0.Output, args: ["retry"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], viewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }], headerScroll: [{ type: i0.ViewChild, args: ['headerScroll', { isSignal: true }] }] } });
|
|
640
|
+
UiTranslatePipe,
|
|
641
|
+
], template: "<!-- role=\"table\", not \"grid\": row access is Tab-only with no arrow-key cell traversal, so \"grid\" would\n promise 2D keyboard navigation this does not implement. Selection is conveyed by the row's checkbox,\n not `aria-selected` \u2014 which is valid only on grid/treegrid anyway. -->\n<div\n class=\"ui-ist\"\n role=\"table\"\n [class.ui-ist--condensed]=\"isCondensed()\"\n [class.ui-ist--scrolled-start]=\"!scrollStart()\"\n [class.ui-ist--scrolled-end]=\"horizontalScroll() && !scrollEnd()\"\n [class.ui-ist--fill-height]=\"fillHeight()\"\n [attr.aria-rowcount]=\"hasMore() ? -1 : data().length + 1\"\n [attr.aria-busy]=\"loading() || loadingMore()\"\n>\n <!-- Pinned header, outside the viewport so it never scrolls vertically. The outer element reserves the\n body's scrollbar width as padding (keeping the background and border across that strip); the inner\n one carries the horizontal scrollLeft synced from the body. -->\n <div class=\"ui-ist__header\" [style.padding-right.px]=\"scrollbarWidth()\" role=\"presentation\">\n <div #headerScroll class=\"ui-ist__header-scroll\" role=\"presentation\">\n <div class=\"ui-ist__row ui-ist__row--header\" role=\"row\" aria-rowindex=\"1\">\n @if (selectable()) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n [class.ui-ist__cell--sticky-start-edge]=\"!stickyStartKey()\"\n role=\"columnheader\"\n ></div>\n }\n @for (column of columns(); track column.key; let colIndex = $index) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header\"\n role=\"columnheader\"\n [class.ui-ist__cell--sticky-start]=\"column.key === stickyStartKey()\"\n [class.ui-ist__cell--sticky-start-edge]=\"isStickyStartEdge(column)\"\n [class.ui-ist__cell--sticky-end]=\"column.key === stickyEndKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left]=\"stickyStartLeft(column)\"\n >\n @if (column.headerCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.headerCellTemplate; context: { $implicit: column.title, column, colIndex }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ column.title }}</span>\n }\n </div>\n }\n @for (loadingKey of loadingColumns(); track loadingKey) {\n <div\n class=\"ui-ist__cell ui-ist__cell--header ui-ist__cell--loading\"\n role=\"presentation\"\n aria-hidden=\"true\"\n [style.flex-basis]=\"defaultColumnWidth\"\n [style.min-width]=\"defaultColumnWidth\"\n [style.max-width]=\"defaultColumnWidth\"\n >\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </div>\n </div>\n\n <!-- The CDK viewport owns BOTH vertical (virtualized) and horizontal scroll, so sticky columns pin\n against it and the horizontal scrollbar sits at the viewport bottom. -->\n @if (showViewport()) {\n <cdk-virtual-scroll-viewport\n class=\"ui-ist__body\"\n role=\"rowgroup\"\n [itemSize]=\"rowHeight()\"\n [minBufferPx]=\"effectiveMinBufferPx()\"\n [maxBufferPx]=\"effectiveMaxBufferPx()\"\n [style.height]=\"viewportHeight()\"\n [style.max-height]=\"fillHeight() ? maxHeight() : null\"\n >\n <div\n *cdkVirtualFor=\"let row of renderRows(); let rowIndex = index; trackBy: renderRowTrackBy\"\n class=\"ui-ist__row\"\n role=\"row\"\n [class.ui-ist__row--clickable]=\"rowsClickable() && row.kind === 'data'\"\n [class.ui-ist__row--selected]=\"isRowSelected(row)\"\n [attr.aria-rowindex]=\"row.kind === 'data' ? row.index + 2 : null\"\n [attr.aria-hidden]=\"row.kind === 'skeleton' ? true : null\"\n [attr.tabindex]=\"rowsClickable() && row.kind === 'data' ? 0 : null\"\n [style.height.px]=\"rowHeight()\"\n (click)=\"emitRowClick($event, row)\"\n (keydown.enter)=\"activateRow($event, row)\"\n (keydown.space)=\"activateRow($event, row)\"\n >\n @if (selectable()) {\n <!-- This `stopPropagation` is what actually stops a checkbox click activating the row: it runs\n before the click reaches the row's listener, so `isInteractiveTarget` is never consulted on\n this path. That guard is a real second line of defence (`ui-checkbox` renders a focusable\n wrapper and a native input, both matching INTERACTIVE_TARGET_SELECTOR), not a substitute. -->\n <div\n class=\"ui-ist__cell ui-ist__cell--checkbox ui-ist__cell--sticky-start\"\n [class.ui-ist__cell--sticky-start-edge]=\"!stickyStartKey()\"\n role=\"cell\"\n (click)=\"$event.stopPropagation()\"\n (keydown)=\"$event.stopPropagation()\"\n >\n @if (row.kind === 'data') {\n <ui-checkbox\n [checked]=\"isRowSelected(row)\"\n [ariaLabel]=\"'Select row ' + (row.index + 1)\"\n (changed)=\"emitSelectionChange(row, $event)\"\n ></ui-checkbox>\n }\n </div>\n }\n @for (column of columns(); track column.key) {\n <div\n class=\"ui-ist__cell\"\n role=\"cell\"\n [class.ui-ist__cell--sticky-start]=\"column.key === stickyStartKey()\"\n [class.ui-ist__cell--sticky-start-edge]=\"isStickyStartEdge(column)\"\n [class.ui-ist__cell--sticky-end]=\"column.key === stickyEndKey()\"\n [class.ui-ist__cell--right]=\"column.styles?.alignment === 'right'\"\n [class.ui-ist__cell--center]=\"column.styles?.alignment === 'center'\"\n [style.flex-basis]=\"columnWidth(column)\"\n [style.min-width]=\"columnWidth(column)\"\n [style.max-width]=\"columnWidth(column)\"\n [style.left]=\"stickyStartLeft(column)\"\n >\n @if (row.kind === 'skeleton') {\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n } @else if (column.rowCellTemplate) {\n <ng-container\n *ngTemplateOutlet=\"column.rowCellTemplate; context: { $implicit: row.item, rowIndex: row.index }\"\n ></ng-container>\n } @else {\n <span class=\"ui-ist__cell-text\">{{ getCellValue(row.item, column.key) }}</span>\n }\n </div>\n }\n @for (loadingKey of loadingColumns(); track loadingKey) {\n <div\n class=\"ui-ist__cell ui-ist__cell--loading\"\n role=\"presentation\"\n aria-hidden=\"true\"\n [style.flex-basis]=\"defaultColumnWidth\"\n [style.min-width]=\"defaultColumnWidth\"\n [style.max-width]=\"defaultColumnWidth\"\n >\n <ui-skeleton class=\"ui-ist__skeleton\" [count]=\"1\" [theme]=\"skeletonTheme\" appearance=\"line\"></ui-skeleton>\n </div>\n }\n <div class=\"ui-ist__cell ui-ist__cell--spacer\" role=\"presentation\" aria-hidden=\"true\"></div>\n </div>\n </cdk-virtual-scroll-viewport>\n }\n\n <!-- `showErrorFooter()` and `showErrorState()` differ only on `hasData()`, so they are mutually\n exclusive and safe to merge here. Both fall back to a plain-language message, so a failed fetch is\n never a silent blank box. -->\n @if (showErrorFooter() || showErrorState()) {\n <div class=\"ui-ist__state\" role=\"alert\">\n @if (errorTemplate(); as errorTpl) {\n <ng-container *ngTemplateOutlet=\"errorTpl; context: errorContext\"></ng-container>\n } @else {\n <span>{{ translationContext + 'LOAD_ERROR' | uiTranslate | async }}</span>\n }\n </div>\n }\n\n @if (showEmptyState()) {\n <div class=\"ui-ist__state\">\n @if (noDataTemplate(); as emptyTpl) {\n <ng-container *ngTemplateOutlet=\"emptyTpl\"></ng-container>\n } @else {\n <span>{{ translationContext + 'NO_ROWS' | uiTranslate | async }}</span>\n }\n </div>\n }\n\n <!-- Opt-in, with no fallback (see `endOfListTemplate`). Not role=\"alert\": reaching the end is not an\n alert, and `aria-rowcount` already conveys completeness to assistive tech. -->\n @if (showEndOfList() && endOfListTemplate(); as endTpl) {\n <div class=\"ui-ist__state\">\n <ng-container *ngTemplateOutlet=\"endTpl\"></ng-container>\n </div>\n }\n\n <span class=\"ui-ist__sr-only\" role=\"status\" aria-live=\"polite\">\n @if (statusMessageKey(); as statusKey) {\n {{ statusKey | uiTranslate | async }}\n }\n </span>\n</div>\n", styles: [".bg-teal-60b{background:#1c443c}.bg-teal-30b{background:#31766a}.bg-teal-default{background:#46a997}.bg-teal-30w{background:#7ec3b6}.bg-teal-60w{background:#b5ddd5}.bg-teal-secondary{background:#cbd6cb}.bg-teal-90w{background:#ecf6f5}.bg-petrol-60b{background:#102930}.bg-petrol-30b{background:#1b4754}.bg-petrol-default{background:#276678}.bg-petrol-30w{background:#6894a0}.bg-petrol-60w{background:#a9c2c9}.bg-petrol-secondary{background:#c8d7de}.bg-petrol-90w{background:#e9f0f1}.bg-error-60b{background:#513131}.bg-error-30b{background:#8e5655}.bg-error-60w{background:#e3c3c6}.bg-error-secondary{background:#f0dad9}.bg-error-default{background:#cb7b7a}.bg-warning-secondary{background:#f0d6bb}.bg-warning-default{background:#cca45f}.bg-black{background:#000}.bg-dark{background:#888}.bg-medium{background:#e0e0e0}.bg-grey{background:#ededed}.bg-light{background:#f6f6f6}.bg-white{background:#fff}.bg-box-shadow{background:#00000014}.bg-navigation-subtitle{background:#528593}.bgc-teal-60b{background-color:#1c443c}.bgc-teal-30b{background-color:#31766a}.bgc-teal-default{background-color:#46a997}.bgc-teal-30w{background-color:#7ec3b6}.bgc-teal-60w{background-color:#b5ddd5}.bgc-teal-secondary{background-color:#cbd6cb}.bgc-teal-90w{background-color:#ecf6f5}.bgc-petrol-60b{background-color:#102930}.bgc-petrol-30b{background-color:#1b4754}.bgc-petrol-default{background-color:#276678}.bgc-petrol-30w{background-color:#6894a0}.bgc-petrol-60w{background-color:#a9c2c9}.bgc-petrol-secondary{background-color:#c8d7de}.bgc-petrol-90w{background-color:#e9f0f1}.bgc-error-60b{background-color:#513131}.bgc-error-30b{background-color:#8e5655}.bgc-error-60w{background-color:#e3c3c6}.bgc-error-secondary{background-color:#f0dad9}.bgc-error-default{background-color:#cb7b7a}.bgc-warning-secondary{background-color:#f0d6bb}.bgc-warning-default{background-color:#cca45f}.bgc-black{background-color:#000}.bgc-dark{background-color:#888}.bgc-medium{background-color:#e0e0e0}.bgc-grey{background-color:#ededed}.bgc-light{background-color:#f6f6f6}.bgc-white{background-color:#fff}.bgc-box-shadow{background-color:#00000014}.bgc-navigation-subtitle{background-color:#528593}:host{display:block}.ui-ist{--ui-ist-checkbox-width: 56px;--ui-ist-skeleton-bg: #f4f4f4;display:flex;flex-direction:column;border:1px solid #e0e0e0;border-radius:8px;overflow:hidden;background:#fff}.ui-ist__header{flex:0 0 auto;box-sizing:border-box;overflow:hidden;border-bottom:1px solid #e0e0e0;background:var(--ui-ist-header-bg, #ffffff)}.ui-ist__header-scroll{overflow:hidden}.ui-ist__body{flex:1 1 auto;overflow:auto;scrollbar-width:auto}.ui-ist__body::-webkit-scrollbar{width:8px;height:8px}.ui-ist__body::-webkit-scrollbar-button{display:none}.ui-ist__body::-webkit-scrollbar-thumb{background-color:#00000059;border-radius:4px}.ui-ist__body::-webkit-scrollbar-track:hover{background-color:#00000026}.ui-ist__row{display:flex;width:max-content;min-width:100%;box-sizing:border-box}.ui-ist__row--header{min-height:56px}.ui-ist__row:not(.ui-ist__row--header){border-bottom:1px solid #d3d3d3}.ui-ist__row--clickable{cursor:pointer}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover{background:#fff2fc}.ui-ist__row--clickable:not(.ui-ist__row--selected):hover .ui-ist__cell{background:#fff2fc}.ui-ist__row--selected .ui-ist__cell{background:#f4f4f4}.ui-ist__cell{flex-grow:0;flex-shrink:0;box-sizing:border-box;display:flex;align-items:center;padding:8px 24px;font-size:14px;line-height:20px;overflow:hidden;background:#fff}.ui-ist__cell--header{background:var(--ui-ist-header-bg, #ffffff);font-weight:700;font-size:14px;line-height:16px}.ui-ist__cell--right{justify-content:flex-end;text-align:right}.ui-ist__cell--center{justify-content:center;text-align:center}.ui-ist__cell--sticky-start{position:sticky;z-index:2}.ui-ist__cell--checkbox.ui-ist__cell--sticky-start{left:0}.ui-ist__cell--sticky-end{position:sticky;right:0;z-index:2;order:1}.ui-ist__cell--checkbox{flex:0 0 var(--ui-ist-checkbox-width);min-width:var(--ui-ist-checkbox-width);justify-content:center;padding:0}.ui-ist__cell--spacer{flex:1 1 auto;min-width:0;padding:0}.ui-ist__row--header .ui-ist__cell--spacer{background:var(--ui-ist-header-bg, #ffffff)}.ui-ist__cell-text{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ui-ist__skeleton{display:block;width:100%}.ui-ist--scrolled-start .ui-ist__cell--sticky-start-edge{box-shadow:24px 8px 24px #24242414}.ui-ist--scrolled-end .ui-ist__cell--sticky-end{box-shadow:0 8px 24px 4px #24242414}.ui-ist__state{padding:8px 24px;font-size:14px;line-height:20px}.ui-ist--condensed .ui-ist__cell{padding:8px 12px}.ui-ist--condensed .ui-ist__cell:first-child{padding-left:24px}.ui-ist--condensed .ui-ist__cell--sticky-end{padding-right:24px}.ui-ist--condensed .ui-ist__cell--spacer{padding:0}:host(.ui-ist--fill-host){height:100%}.ui-ist--fill-height{height:100%}.ui-ist--fill-height .ui-ist__body{min-height:0}.ui-ist__sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}\n"] }]
|
|
642
|
+
}], ctorParameters: () => [], propDecorators: { data: [{ type: i0.Input, args: [{ isSignal: true, alias: "data", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: true }] }], rowHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowHeight", required: true }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], rowIdKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowIdKey", required: false }] }], minBufferPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "minBufferPx", required: false }] }], maxBufferPx: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxBufferPx", required: false }] }], maxHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxHeight", required: false }] }], fillHeight: [{ type: i0.Input, args: [{ isSignal: true, alias: "fillHeight", required: false }] }], isCondensed: [{ type: i0.Input, args: [{ isSignal: true, alias: "isCondensed", required: false }] }], selectable: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectable", required: false }] }], rowsClickable: [{ type: i0.Input, args: [{ isSignal: true, alias: "rowsClickable", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], loadingMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingMore", required: false }] }], hasMore: [{ type: i0.Input, args: [{ isSignal: true, alias: "hasMore", required: true }] }], skeletonRowCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "skeletonRowCount", required: false }] }], loadMoreSkeletonCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadMoreSkeletonCount", required: false }] }], loadingColumnCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadingColumnCount", required: false }] }], loadMoreThreshold: [{ type: i0.Input, args: [{ isSignal: true, alias: "loadMoreThreshold", required: false }] }], noDataTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "noDataTemplate", required: false }] }], errorTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "errorTemplate", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], endOfListTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "endOfListTemplate", required: false }] }], loadMore: [{ type: i0.Output, args: ["loadMore"] }], retry: [{ type: i0.Output, args: ["retry"] }], rowClick: [{ type: i0.Output, args: ["rowClick"] }], selectionChange: [{ type: i0.Output, args: ["selectionChange"] }], viewport: [{ type: i0.ViewChild, args: [i0.forwardRef(() => CdkVirtualScrollViewport), { isSignal: true }] }], headerScroll: [{ type: i0.ViewChild, args: ['headerScroll', { isSignal: true }] }] } });
|
|
438
643
|
|
|
439
644
|
class InfiniteScrollTableComponentModule {
|
|
440
645
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.19", ngImport: i0, type: InfiniteScrollTableComponentModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
|