@astronox/core 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -30
- package/package.json +7 -1
- package/src/admin/contracts.ts +28 -0
- package/src/admin/index.ts +2 -0
- package/src/admin/module.ts +28 -0
- package/src/auth/contracts.ts +53 -0
- package/src/auth/firebase/client.ts +53 -0
- package/src/auth/firebase/contracts.ts +8 -0
- package/src/auth/firebase/index.ts +1 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/module.ts +99 -0
- package/src/client/index.ts +3 -0
- package/src/client/preferences.ts +400 -0
- package/src/components/actions/Button/Button.astro +42 -6
- package/src/components/actions/Button/Button.css +19 -0
- package/src/components/actions/IdentityProviderAction/IdentityProviderAction.astro +35 -0
- package/src/components/actions/MediaTrigger/MediaTrigger.astro +12 -4
- package/src/components/actions/MediaTrigger/MediaTrigger.css +1 -0
- package/src/components/content/CardBody/CardBody.astro +3 -0
- package/src/components/content/CardBody/CardBody.css +10 -0
- package/src/components/content/MarkdownContent/MarkdownContent.css +0 -1
- package/src/components/content/MarkdownContent/MarkdownNode.astro +1 -0
- package/src/components/content/MarkdownContent/markdownContentRenderer.ts +549 -0
- package/src/components/feedback/PreferenceBootstrap/PreferenceBootstrap.astro +403 -0
- package/src/components/forms/Checkbox/Checkbox.astro +66 -0
- package/src/components/forms/Checkbox/Checkbox.css +126 -0
- package/src/components/forms/ChoiceGroup/ChoiceGroup.astro +57 -0
- package/src/components/forms/ChoiceGroup/ChoiceGroup.css +108 -0
- package/src/components/forms/ChoiceGroup/contracts.ts +9 -0
- package/src/components/forms/DateTimeInput/DateTimeInput.astro +14 -0
- package/src/components/forms/FieldGroup/FieldGroup.astro +34 -0
- package/src/components/forms/FieldGroup/FieldGroup.css +30 -0
- package/src/components/forms/TextInput/TextInput.astro +7 -1
- package/src/components/forms/Textarea/Textarea.astro +32 -0
- package/src/components/forms/Textarea/Textarea.css +54 -0
- package/src/components/icons/BackIcon.astro +26 -0
- package/src/components/icons/CheckIcon.astro +22 -0
- package/src/components/icons/DeleteIcon.astro +22 -0
- package/src/components/icons/StatusIndicatorIcon.astro +16 -0
- package/src/components/index.ts +12 -0
- package/src/components/media/CanvasFrame/CanvasFrame.astro +1 -17
- package/src/components/media/EmbedFrame/EmbedFrame.astro +1 -19
- package/src/components/media/EmbedFrame/EmbedFrame.css +0 -2
- package/src/components/navigation/SubNavigation/SubNavigation.css +0 -5
- package/src/composition/Grid/Grid.astro +9 -0
- package/src/composition/Grid/Grid.css +52 -0
- package/src/composition/GridItem/GridItem.astro +9 -0
- package/src/composition/GridItem/GridItem.css +12 -0
- package/src/composition/SurfaceGroup/SurfaceGroup.css +21 -7
- package/src/composition/SurfaceGroup/stickySurfaceGroupController.ts +3 -0
- package/src/data/firestore/client/contracts.ts +55 -0
- package/src/data/firestore/client/firebase.ts +12 -0
- package/src/data/firestore/client/index.ts +3 -0
- package/src/data/firestore/client/repository.ts +111 -0
- package/src/display/contracts.ts +41 -0
- package/src/display/index.ts +1 -0
- package/src/display/tableProgressive.ts +62 -0
- package/src/patterns/Combobox/Combobox.astro +23 -0
- package/src/patterns/Combobox/comboboxController.ts +72 -0
- package/src/patterns/Fullscreen/FullscreenControl.astro +4 -14
- package/src/patterns/Fullscreen/FullscreenControl.css +0 -26
- package/src/patterns/Modal/Modal.astro +1 -1
- package/src/patterns/Modal/Modal.css +38 -1
- package/src/patterns/Table/Table.astro +122 -7
- package/src/patterns/Table/Table.css +2 -1
- package/src/patterns/Table/TableBody.astro +11 -1
- package/src/patterns/Table/TableBody.css +6 -0
- package/src/patterns/Table/TableCell.css +6 -0
- package/src/patterns/Table/TableCellValue.astro +31 -0
- package/src/patterns/Table/TableFooter.astro +3 -1
- package/src/patterns/Table/TableHeader.astro +1 -0
- package/src/patterns/Table/TablePagination.astro +63 -47
- package/src/patterns/Table/TableSkeleton.astro +18 -22
- package/src/patterns/Table/TableSkeleton.css +30 -5
- package/src/patterns/Table/tableClientController.ts +253 -0
- package/src/patterns/Table/tableFilterController.ts +83 -33
- package/src/patterns/Table/tableLayout.ts +10 -3
- package/src/patterns/Table/tableProgressiveController.ts +615 -227
- package/src/patterns/Table/tableSortController.ts +25 -7
- package/src/styles/interaction.css +16 -0
- package/src/styles/style.css +4 -0
|
@@ -1,319 +1,707 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TableProgressiveEntry,
|
|
3
|
+
TableProgressiveModel,
|
|
4
|
+
} from '../../display/contracts';
|
|
5
|
+
import { tableProgressiveBatchSize } from '../../display/tableProgressive';
|
|
6
|
+
|
|
1
7
|
const initializedAttribute = 'data-table-progressive-initialized';
|
|
2
8
|
|
|
3
|
-
type
|
|
9
|
+
type PaginationState = 'idle' | 'loading' | 'error' | 'complete';
|
|
10
|
+
type TableLoadingState =
|
|
11
|
+
| 'pending'
|
|
12
|
+
| 'ready'
|
|
13
|
+
| 'refreshing'
|
|
14
|
+
| 'appending'
|
|
15
|
+
| 'empty'
|
|
16
|
+
| 'error';
|
|
17
|
+
type SortDirection = 'ascending' | 'descending';
|
|
18
|
+
type SortType = 'number' | 'text';
|
|
19
|
+
type SortValue = string | number | null;
|
|
4
20
|
|
|
5
21
|
export type TableRowsAppendedDetail = {
|
|
6
22
|
rows: readonly HTMLElement[];
|
|
7
23
|
};
|
|
8
24
|
|
|
25
|
+
export type TableProgressiveFilterState = {
|
|
26
|
+
search: string;
|
|
27
|
+
selections: Readonly<Record<string, string>>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type TableProgressiveSortState = {
|
|
31
|
+
id: string;
|
|
32
|
+
type: SortType;
|
|
33
|
+
direction: SortDirection;
|
|
34
|
+
tieBreaker?: string;
|
|
35
|
+
} | null;
|
|
36
|
+
|
|
37
|
+
export type TableProgressiveEntryUpdate = {
|
|
38
|
+
sortValues?: Readonly<Record<string, SortValue>>;
|
|
39
|
+
exclusion?: {
|
|
40
|
+
key: string;
|
|
41
|
+
excluded: boolean;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type IndexedEntry = TableProgressiveEntry & {
|
|
46
|
+
index: number;
|
|
47
|
+
exclusions: Set<string>;
|
|
48
|
+
normalizedSearch: readonly string[];
|
|
49
|
+
sortValues: Readonly<Record<string, SortValue>>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
type ProgressiveContext = {
|
|
53
|
+
destroy(): void;
|
|
54
|
+
progressive: true;
|
|
55
|
+
refresh(): void;
|
|
56
|
+
setFilters(state: TableProgressiveFilterState): void;
|
|
57
|
+
setSort(state: TableProgressiveSortState): void;
|
|
58
|
+
updateEntry(key: string, update: TableProgressiveEntryUpdate): void;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const contexts = new WeakMap<HTMLElement, ProgressiveContext>();
|
|
62
|
+
|
|
9
63
|
export function initTableProgressive(root: HTMLElement): () => void {
|
|
10
64
|
if (root.getAttribute(initializedAttribute) === 'true') {
|
|
11
65
|
return () => undefined;
|
|
12
66
|
}
|
|
13
67
|
|
|
14
|
-
const
|
|
68
|
+
const manifest = readManifest(root);
|
|
15
69
|
const rowsRoot = root.querySelector<HTMLElement>('[data-table-rows]');
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
'[data-table-pagination-link]',
|
|
70
|
+
const skeletonTemplate = root.querySelector<HTMLTemplateElement>(
|
|
71
|
+
':scope > [data-table-skeleton-template]',
|
|
19
72
|
);
|
|
73
|
+
|
|
74
|
+
if (!rowsRoot || !skeletonTemplate) {
|
|
75
|
+
return () => undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!manifest) {
|
|
79
|
+
if (root.dataset.tablePreparation !== 'controlled') {
|
|
80
|
+
revealServerRows(root, rowsRoot);
|
|
81
|
+
}
|
|
82
|
+
return () => undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const pagination = root.querySelector<HTMLElement>('[data-table-pagination]');
|
|
20
86
|
const sentinel = pagination?.querySelector<HTMLElement>(
|
|
21
87
|
'[data-table-pagination-sentinel]',
|
|
22
88
|
);
|
|
23
89
|
const status = pagination?.querySelector<HTMLElement>(
|
|
24
90
|
'[data-table-pagination-status]',
|
|
25
91
|
);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
92
|
+
if (!pagination || !sentinel || !status) {
|
|
93
|
+
return () => undefined;
|
|
94
|
+
}
|
|
29
95
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
!('IntersectionObserver' in window)
|
|
39
|
-
) {
|
|
96
|
+
const fallbackLink = pagination.querySelector<HTMLAnchorElement>(
|
|
97
|
+
'[data-table-pagination-link]',
|
|
98
|
+
);
|
|
99
|
+
if (!('IntersectionObserver' in window)) {
|
|
100
|
+
if (root.dataset.tablePreparation !== 'controlled') {
|
|
101
|
+
revealServerRows(root, rowsRoot);
|
|
102
|
+
}
|
|
103
|
+
showFallback(pagination, fallbackLink);
|
|
40
104
|
return () => undefined;
|
|
41
105
|
}
|
|
42
106
|
|
|
43
107
|
root.setAttribute(initializedAttribute, 'true');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
108
|
+
pagination.hidden = false;
|
|
109
|
+
pagination.setAttribute('data-table-progressive-enhanced', 'true');
|
|
110
|
+
pagination.removeAttribute('data-surface');
|
|
111
|
+
|
|
112
|
+
const locale = document.documentElement.lang || undefined;
|
|
113
|
+
const normalize = (value: string): string =>
|
|
114
|
+
value
|
|
115
|
+
.normalize('NFD')
|
|
116
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
117
|
+
.toLocaleLowerCase(locale);
|
|
118
|
+
const entries = manifest.entries.map(
|
|
119
|
+
(entry, index): IndexedEntry => ({
|
|
120
|
+
...entry,
|
|
121
|
+
index,
|
|
122
|
+
exclusions: new Set(),
|
|
123
|
+
normalizedSearch: (entry.filters?.search ?? []).map(normalize),
|
|
124
|
+
sortValues: entry.sortValues ?? {},
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
const entriesByKey = new Map(entries.map((entry) => [entry.key, entry]));
|
|
128
|
+
const rowsByKey = new Map<string, HTMLElement>();
|
|
129
|
+
const loadedSources = new Set<string>();
|
|
130
|
+
const batchSize = Math.max(
|
|
131
|
+
1,
|
|
132
|
+
Math.floor(manifest.batchSize ?? tableProgressiveBatchSize),
|
|
133
|
+
);
|
|
134
|
+
const initialOffset = Math.min(
|
|
135
|
+
Math.max(0, Math.floor(manifest.initialOffset ?? 0)),
|
|
136
|
+
entries.length,
|
|
137
|
+
);
|
|
138
|
+
const empty = root.querySelector<HTMLElement>('[data-table-filter-empty]');
|
|
139
|
+
const collator = new Intl.Collator(locale, {
|
|
140
|
+
numeric: true,
|
|
141
|
+
sensitivity: 'base',
|
|
142
|
+
});
|
|
143
|
+
let filterState: TableProgressiveFilterState = {
|
|
144
|
+
search: '',
|
|
145
|
+
selections: {},
|
|
146
|
+
};
|
|
147
|
+
let sortState: TableProgressiveSortState = null;
|
|
148
|
+
let results = entries;
|
|
149
|
+
let cursor = 0;
|
|
52
150
|
let request: AbortController | null = null;
|
|
53
|
-
let skeletonRows
|
|
54
|
-
let batchedRows: HTMLElement[] = [];
|
|
151
|
+
let skeletonRows = getSkeletonRows(rowsRoot);
|
|
55
152
|
let disposed = false;
|
|
56
|
-
let
|
|
57
|
-
let
|
|
153
|
+
let generation = 0;
|
|
154
|
+
let prepared = false;
|
|
155
|
+
|
|
156
|
+
registerRows(getRows(rowsRoot));
|
|
58
157
|
|
|
59
158
|
const observer = new IntersectionObserver(
|
|
60
|
-
(
|
|
61
|
-
if (
|
|
62
|
-
void
|
|
159
|
+
(observedEntries) => {
|
|
160
|
+
if (observedEntries.some((entry) => entry.isIntersecting)) {
|
|
161
|
+
void renderNextBatch();
|
|
63
162
|
}
|
|
64
163
|
},
|
|
65
164
|
{ rootMargin: '600px 0px' },
|
|
66
165
|
);
|
|
67
166
|
|
|
68
|
-
|
|
167
|
+
function registerRows(rows: readonly HTMLElement[]): void {
|
|
168
|
+
for (const row of rows) {
|
|
169
|
+
const key = row.dataset.tableRowKey;
|
|
170
|
+
if (key) rowsByKey.set(key, row);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function removeSkeleton(): void {
|
|
69
175
|
skeletonRows.forEach((row) => row.remove());
|
|
70
176
|
skeletonRows = [];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const addSkeleton = (): void => {
|
|
74
|
-
if (skeletonRows.length > 0) return;
|
|
177
|
+
}
|
|
75
178
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
179
|
+
function addSkeleton(count: number): void {
|
|
180
|
+
removeSkeleton();
|
|
181
|
+
const candidates = Array.from(
|
|
182
|
+
skeletonTemplate.content.querySelectorAll<HTMLElement>(
|
|
183
|
+
'[data-table-skeleton-row]',
|
|
184
|
+
),
|
|
185
|
+
).slice(0, count);
|
|
186
|
+
skeletonRows = candidates.map((row) => document.importNode(row, true));
|
|
187
|
+
insertBeforeEmpty(rowsRoot, empty, skeletonRows);
|
|
188
|
+
}
|
|
79
189
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
}
|
|
190
|
+
function detachRenderedRows(): HTMLElement[] {
|
|
191
|
+
const rendered = getRows(rowsRoot);
|
|
192
|
+
rendered.forEach((row) => row.remove());
|
|
193
|
+
return rendered;
|
|
194
|
+
}
|
|
85
195
|
|
|
86
|
-
|
|
196
|
+
function appendRows(rows: readonly HTMLElement[]): void {
|
|
87
197
|
if (rows.length === 0) return;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (const row of rows) fragment.append(row);
|
|
91
|
-
rowsRoot.append(fragment);
|
|
198
|
+
for (const row of rows) row.hidden = false;
|
|
199
|
+
insertBeforeEmpty(rowsRoot, empty, rows);
|
|
92
200
|
root.dispatchEvent(
|
|
93
201
|
new CustomEvent<TableRowsAppendedDetail>('table:rows-appended', {
|
|
94
202
|
bubbles: true,
|
|
95
203
|
detail: { rows },
|
|
96
204
|
}),
|
|
97
205
|
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const flushBatchedRows = (): void => {
|
|
101
|
-
if (batchedRows.length === 0) {
|
|
102
|
-
removeSkeleton();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
206
|
+
}
|
|
105
207
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
appendRows(rows);
|
|
110
|
-
};
|
|
208
|
+
function updateEmptyState(): void {
|
|
209
|
+
if (empty) empty.hidden = results.length > 0;
|
|
210
|
+
}
|
|
111
211
|
|
|
112
|
-
|
|
113
|
-
completed = true;
|
|
114
|
-
flushBatchedRows();
|
|
115
|
-
loadAllRequested = false;
|
|
212
|
+
function updatePagination(): void {
|
|
116
213
|
observer.disconnect();
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
214
|
+
const complete = cursor >= results.length;
|
|
215
|
+
pagination.hidden = complete;
|
|
216
|
+
setPaginationState(pagination, status, complete ? 'complete' : 'idle');
|
|
217
|
+
if (!complete) observer.observe(sentinel);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function updateTableState(state: TableLoadingState): void {
|
|
221
|
+
root.dataset.tableLoadingState = state;
|
|
222
|
+
if (
|
|
223
|
+
state === 'pending' ||
|
|
224
|
+
state === 'refreshing' ||
|
|
225
|
+
state === 'appending'
|
|
226
|
+
) {
|
|
227
|
+
root.setAttribute('aria-busy', 'true');
|
|
228
|
+
} else {
|
|
229
|
+
root.removeAttribute('aria-busy');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function applyQuery(): void {
|
|
234
|
+
const query = normalize(filterState.search.trim());
|
|
235
|
+
const selections = Object.entries(filterState.selections).filter(
|
|
236
|
+
([, value]) => value.length > 0,
|
|
123
237
|
);
|
|
124
|
-
};
|
|
125
238
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
239
|
+
results = entries.filter((entry) => {
|
|
240
|
+
if (entry.exclusions.size > 0) return false;
|
|
241
|
+
if (
|
|
242
|
+
query.length > 0 &&
|
|
243
|
+
!entry.normalizedSearch.some((value) => value.includes(query))
|
|
244
|
+
) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return selections.every(([field, value]) =>
|
|
248
|
+
(entry.filters?.[field] ?? []).includes(value),
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
if (sortState) {
|
|
253
|
+
const state = sortState;
|
|
254
|
+
results = [...results].sort((left, right) => {
|
|
255
|
+
const comparison = compareValues(
|
|
256
|
+
left.sortValues[state.id],
|
|
257
|
+
right.sortValues[state.id],
|
|
258
|
+
state.type,
|
|
259
|
+
state.direction,
|
|
260
|
+
collator,
|
|
261
|
+
);
|
|
262
|
+
if (comparison !== 0) return comparison;
|
|
263
|
+
|
|
264
|
+
if (state.tieBreaker) {
|
|
265
|
+
const id = state.tieBreaker;
|
|
266
|
+
const tieComparison = compareValues(
|
|
267
|
+
left.sortValues[id],
|
|
268
|
+
right.sortValues[id],
|
|
269
|
+
inferSortType(left.sortValues[id], right.sortValues[id]),
|
|
270
|
+
'ascending',
|
|
271
|
+
collator,
|
|
272
|
+
);
|
|
273
|
+
if (tieComparison !== 0) return tieComparison;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return left.index - right.index;
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
130
280
|
|
|
131
|
-
async function
|
|
132
|
-
|
|
281
|
+
async function renderNextBatch(
|
|
282
|
+
loadingState: Extract<
|
|
283
|
+
TableLoadingState,
|
|
284
|
+
'pending' | 'refreshing' | 'appending'
|
|
285
|
+
> = 'appending',
|
|
286
|
+
): Promise<void> {
|
|
287
|
+
if (request || disposed || cursor >= results.length) return;
|
|
288
|
+
|
|
289
|
+
const target = results.slice(cursor, cursor + batchSize);
|
|
290
|
+
if (target.length === 0) {
|
|
291
|
+
removeSkeleton();
|
|
292
|
+
updatePagination();
|
|
293
|
+
updateTableState(results.length === 0 ? 'empty' : 'ready');
|
|
133
294
|
return;
|
|
134
295
|
}
|
|
135
296
|
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
297
|
+
const missing = target.filter((entry) => !rowsByKey.has(entry.key));
|
|
298
|
+
const activeGeneration = generation;
|
|
299
|
+
|
|
300
|
+
if (missing.length > 0) {
|
|
301
|
+
const activeRequest = new AbortController();
|
|
302
|
+
request = activeRequest;
|
|
303
|
+
setPaginationState(pagination, status, 'loading');
|
|
304
|
+
updateTableState(loadingState);
|
|
305
|
+
addSkeleton(target.length);
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
await loadSources(missing, activeRequest.signal);
|
|
309
|
+
} catch (error) {
|
|
310
|
+
if (error instanceof DOMException && error.name === 'AbortError') return;
|
|
311
|
+
removeSkeleton();
|
|
312
|
+
setPaginationState(pagination, status, 'error');
|
|
313
|
+
updateTableState('error');
|
|
314
|
+
showFallback(pagination, fallbackLink);
|
|
315
|
+
observer.disconnect();
|
|
316
|
+
return;
|
|
317
|
+
} finally {
|
|
318
|
+
if (request === activeRequest) {
|
|
319
|
+
request = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (disposed || activeGeneration !== generation) return;
|
|
325
|
+
const rows = target.map((entry) => rowsByKey.get(entry.key));
|
|
326
|
+
if (rows.some((row) => !row)) {
|
|
327
|
+
removeSkeleton();
|
|
328
|
+
setPaginationState(pagination, status, 'error');
|
|
329
|
+
updateTableState('error');
|
|
330
|
+
showFallback(pagination, fallbackLink);
|
|
140
331
|
observer.disconnect();
|
|
141
332
|
return;
|
|
142
333
|
}
|
|
143
334
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
335
|
+
removeSkeleton();
|
|
336
|
+
appendRows(rows as HTMLElement[]);
|
|
337
|
+
cursor += target.length;
|
|
338
|
+
updatePagination();
|
|
339
|
+
updateTableState('ready');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function loadSources(
|
|
343
|
+
missingEntries: readonly IndexedEntry[],
|
|
344
|
+
signal: AbortSignal,
|
|
345
|
+
): Promise<void> {
|
|
346
|
+
const sourceIds = new Set(
|
|
347
|
+
missingEntries.map((entry) => entry.source).filter(isString),
|
|
348
|
+
);
|
|
349
|
+
if (sourceIds.size === 0 || !manifest.sources) {
|
|
350
|
+
throw new Error('Missing progressive table row source');
|
|
147
351
|
}
|
|
148
352
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
353
|
+
await Promise.all(
|
|
354
|
+
[...sourceIds].map(async (sourceId) => {
|
|
355
|
+
if (loadedSources.has(sourceId)) return;
|
|
356
|
+
const href = manifest.sources?.[sourceId];
|
|
357
|
+
if (!href) throw new Error(`Missing table source: ${sourceId}`);
|
|
358
|
+
|
|
359
|
+
const url = new URL(href, window.location.href);
|
|
360
|
+
if (url.origin !== window.location.origin) {
|
|
361
|
+
throw new Error('Table sources must use the current origin');
|
|
362
|
+
}
|
|
363
|
+
const response = await fetch(url, {
|
|
364
|
+
credentials: 'same-origin',
|
|
365
|
+
headers: { Accept: 'text/html' },
|
|
366
|
+
signal,
|
|
367
|
+
});
|
|
368
|
+
if (!response.ok) {
|
|
369
|
+
throw new Error(`Unable to load table source: ${response.status}`);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const documentNode = new DOMParser().parseFromString(
|
|
373
|
+
await response.text(),
|
|
374
|
+
'text/html',
|
|
375
|
+
);
|
|
376
|
+
const tableKey = root.dataset.tableKey;
|
|
377
|
+
const sourceTable = tableKey
|
|
378
|
+
? Array.from(
|
|
379
|
+
documentNode.querySelectorAll<HTMLElement>('[data-table-key]'),
|
|
380
|
+
).find((table) => table.dataset.tableKey === tableKey)
|
|
381
|
+
: null;
|
|
382
|
+
const sourceRowsRoot = sourceTable?.querySelector<HTMLElement>(
|
|
383
|
+
'[data-table-rows]',
|
|
384
|
+
);
|
|
385
|
+
const sourceRows = sourceRowsRoot ? getRows(sourceRowsRoot) : [];
|
|
386
|
+
if (sourceRows.length === 0) {
|
|
387
|
+
throw new Error(`Missing progressive table: ${tableKey ?? 'unknown'}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
registerRows(sourceRows.map((row) => document.importNode(row, true)));
|
|
391
|
+
loadedSources.add(sourceId);
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
152
395
|
|
|
153
|
-
|
|
396
|
+
function prepareWindow(
|
|
397
|
+
offset: number,
|
|
398
|
+
state: Extract<TableLoadingState, 'pending' | 'refreshing'>,
|
|
399
|
+
): void {
|
|
400
|
+
generation += 1;
|
|
401
|
+
request?.abort();
|
|
402
|
+
request = null;
|
|
403
|
+
updateTableState(state);
|
|
404
|
+
addSkeleton(batchSize);
|
|
405
|
+
detachRenderedRows();
|
|
406
|
+
applyQuery();
|
|
407
|
+
cursor = Math.min(offset, results.length);
|
|
408
|
+
updateEmptyState();
|
|
409
|
+
if (results.length === 0) {
|
|
410
|
+
removeSkeleton();
|
|
411
|
+
updatePagination();
|
|
412
|
+
updateTableState('empty');
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
void renderNextBatch(state);
|
|
416
|
+
}
|
|
154
417
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
418
|
+
function refresh(): void {
|
|
419
|
+
if (!prepared) {
|
|
420
|
+
prepared = true;
|
|
421
|
+
prepareWindow(initialOffset, 'pending');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
prepareWindow(0, 'refreshing');
|
|
425
|
+
}
|
|
162
426
|
|
|
163
|
-
|
|
164
|
-
|
|
427
|
+
const context: ProgressiveContext = {
|
|
428
|
+
destroy() {
|
|
429
|
+
disposed = true;
|
|
430
|
+
request?.abort();
|
|
431
|
+
observer.disconnect();
|
|
432
|
+
removeSkeleton();
|
|
433
|
+
pagination.hidden = !fallbackLink;
|
|
434
|
+
pagination.removeAttribute('data-table-progressive-enhanced');
|
|
435
|
+
pagination.removeAttribute('data-table-progressive-state');
|
|
436
|
+
if (!pagination.hidden) pagination.setAttribute('data-surface', '');
|
|
437
|
+
status.hidden = true;
|
|
438
|
+
status.textContent = '';
|
|
439
|
+
root.removeAttribute('aria-busy');
|
|
440
|
+
root.removeAttribute('data-table-loading-state');
|
|
441
|
+
root.removeAttribute(initializedAttribute);
|
|
442
|
+
contexts.delete(root);
|
|
443
|
+
},
|
|
444
|
+
progressive: true,
|
|
445
|
+
refresh,
|
|
446
|
+
setFilters(state) {
|
|
447
|
+
if (areFilterStatesEqual(filterState, state)) return;
|
|
448
|
+
filterState = state;
|
|
449
|
+
if (prepared) prepareWindow(0, 'refreshing');
|
|
450
|
+
},
|
|
451
|
+
setSort(state) {
|
|
452
|
+
if (areSortStatesEqual(sortState, state)) return;
|
|
453
|
+
sortState = state;
|
|
454
|
+
if (prepared) prepareWindow(0, 'refreshing');
|
|
455
|
+
},
|
|
456
|
+
updateEntry(key, update) {
|
|
457
|
+
const entry = entriesByKey.get(key);
|
|
458
|
+
if (!entry) return;
|
|
459
|
+
if (update.sortValues) {
|
|
460
|
+
entry.sortValues = { ...entry.sortValues, ...update.sortValues };
|
|
165
461
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
nextDocument.querySelectorAll<HTMLElement>('[data-table-key]'),
|
|
173
|
-
).find((table) => table.dataset.tableKey === tableKey);
|
|
174
|
-
const nextRowsRoot =
|
|
175
|
-
nextTable?.querySelector<HTMLElement>('[data-table-rows]');
|
|
176
|
-
|
|
177
|
-
if (!nextTable || !nextRowsRoot) {
|
|
178
|
-
throw new Error(`Missing paginated table: ${tableKey}`);
|
|
462
|
+
if (update.exclusion) {
|
|
463
|
+
if (update.exclusion.excluded) {
|
|
464
|
+
entry.exclusions.add(update.exclusion.key);
|
|
465
|
+
} else {
|
|
466
|
+
entry.exclusions.delete(update.exclusion.key);
|
|
467
|
+
}
|
|
179
468
|
}
|
|
469
|
+
},
|
|
470
|
+
};
|
|
180
471
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
);
|
|
184
|
-
if (nextRows.length === 0) {
|
|
185
|
-
throw new Error(`Missing paginated table rows: ${tableKey}`);
|
|
186
|
-
}
|
|
472
|
+
contexts.set(root, context);
|
|
473
|
+
if (root.dataset.tablePreparation !== 'controlled') refresh();
|
|
187
474
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
);
|
|
191
|
-
const nextLink = nextPagination?.querySelector<HTMLAnchorElement>(
|
|
192
|
-
'[data-table-pagination-link]',
|
|
193
|
-
);
|
|
475
|
+
return context.destroy;
|
|
476
|
+
}
|
|
194
477
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
loadedUrls.add(requestKey);
|
|
478
|
+
export function setTableProgressiveFilters(
|
|
479
|
+
root: HTMLElement,
|
|
480
|
+
state: TableProgressiveFilterState,
|
|
481
|
+
): boolean {
|
|
482
|
+
const context = getProgressiveContext(root);
|
|
483
|
+
if (!context) return false;
|
|
484
|
+
context.setFilters(state);
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
205
487
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
488
|
+
export function setTableProgressiveSort(
|
|
489
|
+
root: HTMLElement,
|
|
490
|
+
state: TableProgressiveSortState,
|
|
491
|
+
): boolean {
|
|
492
|
+
const context = getProgressiveContext(root);
|
|
493
|
+
if (!context) return false;
|
|
494
|
+
context.setSort(state);
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
210
497
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
498
|
+
export function updateTableProgressiveEntry(
|
|
499
|
+
root: HTMLElement,
|
|
500
|
+
key: string,
|
|
501
|
+
update: TableProgressiveEntryUpdate,
|
|
502
|
+
): boolean {
|
|
503
|
+
const context = getProgressiveContext(root);
|
|
504
|
+
if (!context) return false;
|
|
505
|
+
context.updateEntry(key, update);
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
216
508
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
request = null;
|
|
234
|
-
if (!loadAllRequested) root.removeAttribute('aria-busy');
|
|
235
|
-
if (continueLoading && !disposed) void loadNextPage();
|
|
236
|
-
}
|
|
509
|
+
export function refreshTableProgressive(root: HTMLElement): boolean {
|
|
510
|
+
const context = getProgressiveContext(root);
|
|
511
|
+
if (!context) return false;
|
|
512
|
+
context.refresh();
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function hasTableProgressive(root: HTMLElement): boolean {
|
|
517
|
+
return getProgressiveContext(root)?.progressive === true;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function resolveTablePreparation(root: HTMLElement): void {
|
|
521
|
+
const context = getProgressiveContext(root);
|
|
522
|
+
if (context) {
|
|
523
|
+
context.refresh();
|
|
524
|
+
return;
|
|
237
525
|
}
|
|
238
526
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
root.setAttribute('data-table-progressive-loading-all', 'true');
|
|
243
|
-
void loadNextPage();
|
|
244
|
-
};
|
|
527
|
+
const rowsRoot = root.querySelector<HTMLElement>('[data-table-rows]');
|
|
528
|
+
if (rowsRoot) revealServerRows(root, rowsRoot);
|
|
529
|
+
}
|
|
245
530
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
531
|
+
function getProgressiveContext(
|
|
532
|
+
root: HTMLElement,
|
|
533
|
+
): ProgressiveContext | undefined {
|
|
534
|
+
const existing = contexts.get(root);
|
|
535
|
+
if (existing) return existing;
|
|
536
|
+
if (
|
|
537
|
+
!root.querySelector(':scope > [data-table-progressive-manifest]')
|
|
538
|
+
) {
|
|
539
|
+
return undefined;
|
|
540
|
+
}
|
|
541
|
+
initTableProgressive(root);
|
|
542
|
+
return contexts.get(root);
|
|
543
|
+
}
|
|
257
544
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
545
|
+
function readManifest(root: HTMLElement): TableProgressiveModel | null {
|
|
546
|
+
const node = root.querySelector<HTMLScriptElement>(
|
|
547
|
+
':scope > [data-table-progressive-manifest]',
|
|
548
|
+
);
|
|
549
|
+
if (!node?.textContent) return null;
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const value: unknown = JSON.parse(node.textContent);
|
|
553
|
+
if (!isRecord(value) || !Array.isArray(value.entries)) return null;
|
|
554
|
+
const entries = value.entries.filter(isProgressiveEntry);
|
|
555
|
+
if (entries.length !== value.entries.length) return null;
|
|
556
|
+
return {
|
|
557
|
+
entries,
|
|
558
|
+
sources: isStringRecord(value.sources) ? value.sources : undefined,
|
|
559
|
+
batchSize: isFiniteNumber(value.batchSize) ? value.batchSize : undefined,
|
|
560
|
+
initialOffset: isFiniteNumber(value.initialOffset)
|
|
561
|
+
? value.initialOffset
|
|
562
|
+
: undefined,
|
|
563
|
+
};
|
|
564
|
+
} catch {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
261
568
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
569
|
+
function getRows(root: ParentNode): HTMLElement[] {
|
|
570
|
+
return Array.from(
|
|
571
|
+
root.querySelectorAll<HTMLElement>(':scope > [data-table-row]'),
|
|
572
|
+
);
|
|
573
|
+
}
|
|
266
574
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
575
|
+
function getSkeletonRows(root: ParentNode): HTMLElement[] {
|
|
576
|
+
return Array.from(
|
|
577
|
+
root.querySelectorAll<HTMLElement>(
|
|
578
|
+
':scope > [data-table-skeleton-row]',
|
|
579
|
+
),
|
|
580
|
+
);
|
|
581
|
+
}
|
|
270
582
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
activePagination.setAttribute('data-surface', '');
|
|
280
|
-
activePagination.removeAttribute('data-table-progressive-enhanced');
|
|
281
|
-
activePagination.removeAttribute('data-table-progressive-state');
|
|
282
|
-
activeStatus.hidden = true;
|
|
283
|
-
activeStatus.textContent = '';
|
|
284
|
-
root.removeAttribute('data-table-progressive-loading-all');
|
|
285
|
-
root.removeAttribute('aria-busy');
|
|
286
|
-
root.removeAttribute(initializedAttribute);
|
|
287
|
-
};
|
|
583
|
+
function revealServerRows(root: HTMLElement, rowsRoot: HTMLElement): void {
|
|
584
|
+
getSkeletonRows(rowsRoot).forEach((row) => row.remove());
|
|
585
|
+
root.dataset.tableLoadingState = getRows(rowsRoot).some(
|
|
586
|
+
(row) => !row.hidden,
|
|
587
|
+
)
|
|
588
|
+
? 'ready'
|
|
589
|
+
: 'empty';
|
|
590
|
+
root.removeAttribute('aria-busy');
|
|
288
591
|
}
|
|
289
592
|
|
|
290
|
-
function
|
|
593
|
+
function insertBeforeEmpty(
|
|
594
|
+
rowsRoot: HTMLElement,
|
|
595
|
+
empty: HTMLElement | null,
|
|
596
|
+
rows: readonly HTMLElement[],
|
|
597
|
+
): void {
|
|
598
|
+
const fragment = document.createDocumentFragment();
|
|
599
|
+
rows.forEach((row) => fragment.append(row));
|
|
600
|
+
rowsRoot.insertBefore(fragment, empty);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function setPaginationState(
|
|
291
604
|
pagination: HTMLElement,
|
|
292
605
|
status: HTMLElement,
|
|
293
|
-
state:
|
|
606
|
+
state: PaginationState,
|
|
294
607
|
): void {
|
|
295
608
|
pagination.dataset.tableProgressiveState = state;
|
|
296
609
|
status.hidden = state !== 'loading' && state !== 'error';
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
status.textContent = '';
|
|
304
|
-
}
|
|
610
|
+
status.textContent =
|
|
611
|
+
state === 'loading'
|
|
612
|
+
? (pagination.dataset.tableLoadingLabel ?? '')
|
|
613
|
+
: state === 'error'
|
|
614
|
+
? (pagination.dataset.tableErrorLabel ?? '')
|
|
615
|
+
: '';
|
|
305
616
|
}
|
|
306
617
|
|
|
307
|
-
function
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
key: 'tablePage' | 'tableTotalPages',
|
|
618
|
+
function showFallback(
|
|
619
|
+
pagination: HTMLElement,
|
|
620
|
+
link: HTMLAnchorElement | null,
|
|
311
621
|
): void {
|
|
312
|
-
|
|
622
|
+
if (!link) return;
|
|
623
|
+
pagination.hidden = false;
|
|
624
|
+
pagination.removeAttribute('data-table-progressive-enhanced');
|
|
625
|
+
pagination.setAttribute('data-surface', '');
|
|
626
|
+
}
|
|
313
627
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
628
|
+
function compareValues(
|
|
629
|
+
left: SortValue | undefined,
|
|
630
|
+
right: SortValue | undefined,
|
|
631
|
+
type: SortType,
|
|
632
|
+
direction: SortDirection,
|
|
633
|
+
collator: Intl.Collator,
|
|
634
|
+
): number {
|
|
635
|
+
const leftMissing = left === null || left === undefined;
|
|
636
|
+
const rightMissing = right === null || right === undefined;
|
|
637
|
+
if (leftMissing || rightMissing) {
|
|
638
|
+
if (leftMissing && rightMissing) return 0;
|
|
639
|
+
return leftMissing ? 1 : -1;
|
|
318
640
|
}
|
|
641
|
+
|
|
642
|
+
const comparison =
|
|
643
|
+
type === 'number'
|
|
644
|
+
? Number(left) - Number(right)
|
|
645
|
+
: collator.compare(String(left), String(right));
|
|
646
|
+
return direction === 'ascending' ? comparison : -comparison;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function areFilterStatesEqual(
|
|
650
|
+
left: TableProgressiveFilterState,
|
|
651
|
+
right: TableProgressiveFilterState,
|
|
652
|
+
): boolean {
|
|
653
|
+
if (left.search !== right.search) return false;
|
|
654
|
+
const leftEntries = Object.entries(left.selections);
|
|
655
|
+
const rightEntries = Object.entries(right.selections);
|
|
656
|
+
return (
|
|
657
|
+
leftEntries.length === rightEntries.length &&
|
|
658
|
+
leftEntries.every(([key, value]) => right.selections[key] === value)
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function areSortStatesEqual(
|
|
663
|
+
left: TableProgressiveSortState,
|
|
664
|
+
right: TableProgressiveSortState,
|
|
665
|
+
): boolean {
|
|
666
|
+
if (left === right) return true;
|
|
667
|
+
return Boolean(
|
|
668
|
+
left &&
|
|
669
|
+
right &&
|
|
670
|
+
left.id === right.id &&
|
|
671
|
+
left.type === right.type &&
|
|
672
|
+
left.direction === right.direction &&
|
|
673
|
+
left.tieBreaker === right.tieBreaker,
|
|
674
|
+
);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function inferSortType(
|
|
678
|
+
left: SortValue | undefined,
|
|
679
|
+
right: SortValue | undefined,
|
|
680
|
+
): SortType {
|
|
681
|
+
return typeof left === 'number' || typeof right === 'number'
|
|
682
|
+
? 'number'
|
|
683
|
+
: 'text';
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function isProgressiveEntry(value: unknown): value is TableProgressiveEntry {
|
|
687
|
+
return isRecord(value) && typeof value.key === 'string';
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
691
|
+
return typeof value === 'object' && value !== null;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function isString(value: string | undefined): value is string {
|
|
695
|
+
return typeof value === 'string';
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function isStringRecord(value: unknown): value is Record<string, string> {
|
|
699
|
+
return (
|
|
700
|
+
isRecord(value) &&
|
|
701
|
+
Object.values(value).every((entry) => typeof entry === 'string')
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
706
|
+
return typeof value === 'number' && Number.isFinite(value);
|
|
319
707
|
}
|