@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,70 +1,86 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
-
import type {
|
|
4
|
-
TableColumn,
|
|
5
|
-
TablePaginationModel,
|
|
6
|
-
} from '../../display/contracts';
|
|
3
|
+
import type { TablePaginationModel } from '../../display/contracts';
|
|
7
4
|
import TableFooter from './TableFooter.astro';
|
|
8
|
-
import TableSkeleton from './TableSkeleton.astro';
|
|
9
5
|
|
|
10
6
|
type Props = HTMLAttributes<'div'> & {
|
|
11
|
-
columns: readonly TableColumn[];
|
|
12
7
|
errorLabel: string;
|
|
13
8
|
loadingLabel: string;
|
|
14
9
|
nextLabel: string;
|
|
15
|
-
pagination
|
|
16
|
-
|
|
10
|
+
pagination?: TablePaginationModel;
|
|
11
|
+
progressive?: boolean;
|
|
17
12
|
};
|
|
18
13
|
|
|
19
14
|
const {
|
|
20
15
|
class: className,
|
|
21
|
-
columns,
|
|
22
16
|
errorLabel,
|
|
23
17
|
loadingLabel,
|
|
24
18
|
nextLabel,
|
|
25
19
|
pagination,
|
|
26
|
-
|
|
20
|
+
progressive = false,
|
|
27
21
|
...attrs
|
|
28
22
|
}: Props = Astro.props;
|
|
29
23
|
---
|
|
30
24
|
|
|
31
25
|
{
|
|
32
|
-
pagination
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
26
|
+
pagination?.nextHref || progressive ? (
|
|
27
|
+
<>
|
|
28
|
+
<TableFooter
|
|
29
|
+
{...attrs}
|
|
30
|
+
class:list={['table-pagination', className]}
|
|
31
|
+
padding="none"
|
|
32
|
+
surface={!progressive}
|
|
33
|
+
data-table-pagination
|
|
34
|
+
data-table-loading-label={loadingLabel}
|
|
35
|
+
data-table-error-label={errorLabel}
|
|
36
|
+
data-table-progressive-state="idle"
|
|
37
|
+
hidden={progressive || !pagination?.nextHref}
|
|
38
|
+
>
|
|
39
|
+
<div class="table-pagination__content">
|
|
40
|
+
{
|
|
41
|
+
pagination?.nextHref ? (
|
|
42
|
+
<a
|
|
43
|
+
class="table-pagination__link"
|
|
44
|
+
href={pagination.nextHref}
|
|
45
|
+
data-table-pagination-link
|
|
46
|
+
>
|
|
47
|
+
{nextLabel}
|
|
48
|
+
</a>
|
|
49
|
+
) : null
|
|
50
|
+
}
|
|
51
|
+
<span
|
|
52
|
+
class="table-pagination__status"
|
|
53
|
+
data-table-pagination-status
|
|
54
|
+
role="status"
|
|
55
|
+
aria-live="polite"
|
|
56
|
+
hidden
|
|
57
|
+
/>
|
|
58
|
+
<span
|
|
59
|
+
class="table-pagination__sentinel"
|
|
60
|
+
data-table-pagination-sentinel
|
|
61
|
+
aria-hidden="true"
|
|
62
|
+
/>
|
|
63
|
+
</div>
|
|
64
|
+
</TableFooter>
|
|
65
|
+
{
|
|
66
|
+
progressive && pagination?.nextHref ? (
|
|
67
|
+
<noscript>
|
|
68
|
+
<TableFooter
|
|
69
|
+
class:list={['table-pagination', className]}
|
|
70
|
+
padding="none"
|
|
71
|
+
>
|
|
72
|
+
<div class="table-pagination__content">
|
|
73
|
+
<a
|
|
74
|
+
class="table-pagination__link"
|
|
75
|
+
href={pagination.nextHref}
|
|
76
|
+
>
|
|
77
|
+
{nextLabel}
|
|
78
|
+
</a>
|
|
79
|
+
</div>
|
|
80
|
+
</TableFooter>
|
|
81
|
+
</noscript>
|
|
82
|
+
) : null
|
|
83
|
+
}
|
|
84
|
+
</>
|
|
69
85
|
) : null
|
|
70
86
|
}
|
|
@@ -11,26 +11,22 @@ const { columns, rows = 3 }: Props = Astro.props;
|
|
|
11
11
|
const rowCount = Math.max(1, Math.floor(rows));
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
{
|
|
15
|
+
Array.from({ length: rowCount }, () => (
|
|
16
|
+
<tr
|
|
17
|
+
class="table-skeleton__row"
|
|
18
|
+
data-table-skeleton-row
|
|
19
|
+
aria-hidden="true"
|
|
20
|
+
>
|
|
21
|
+
{columns.map((column) => (
|
|
22
|
+
<td
|
|
23
|
+
class="table-skeleton__cell"
|
|
24
|
+
data-align={getTableColumnAlignment(column)}
|
|
25
|
+
data-table-column-role={column.role}
|
|
22
26
|
>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
<span class="table-skeleton__placeholder" />
|
|
30
|
-
</td>
|
|
31
|
-
))}
|
|
32
|
-
</tr>
|
|
33
|
-
))
|
|
34
|
-
}
|
|
35
|
-
</tbody>
|
|
36
|
-
</table>
|
|
27
|
+
<span class="table-skeleton__placeholder" />
|
|
28
|
+
</td>
|
|
29
|
+
))}
|
|
30
|
+
</tr>
|
|
31
|
+
))
|
|
32
|
+
}
|
|
@@ -1,16 +1,41 @@
|
|
|
1
|
-
.table-skeleton {
|
|
2
|
-
display: none;
|
|
3
|
-
}
|
|
4
|
-
|
|
5
1
|
.table-skeleton__row {
|
|
6
|
-
display:
|
|
2
|
+
display: none;
|
|
7
3
|
min-width: 0;
|
|
8
4
|
min-height: var(--table-row-min-block-size);
|
|
9
5
|
grid-column: 1 / -1;
|
|
10
6
|
grid-template-columns: subgrid;
|
|
7
|
+
border-block-start: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.table-skeleton__row + .table-skeleton__row,
|
|
11
|
+
[data-nox-table-enhanced]
|
|
12
|
+
.table[data-table-loading-state='appending']
|
|
13
|
+
.table-body__row:not([hidden])
|
|
14
|
+
+ .table-skeleton__row {
|
|
11
15
|
border-block-start: var(--nox-border-width) solid var(--nox-border);
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
[data-nox-table-enhanced]
|
|
19
|
+
.table[data-table-loading-state='pending']
|
|
20
|
+
.table-body__row[data-table-row],
|
|
21
|
+
[data-nox-table-enhanced]
|
|
22
|
+
.table[data-table-loading-state='refreshing']
|
|
23
|
+
.table-body__row[data-table-row] {
|
|
24
|
+
display: none;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
[data-nox-table-enhanced]
|
|
28
|
+
.table[data-table-loading-state='pending']
|
|
29
|
+
.table-skeleton__row,
|
|
30
|
+
[data-nox-table-enhanced]
|
|
31
|
+
.table[data-table-loading-state='refreshing']
|
|
32
|
+
.table-skeleton__row,
|
|
33
|
+
[data-nox-table-enhanced]
|
|
34
|
+
.table[data-table-loading-state='appending']
|
|
35
|
+
.table-skeleton__row {
|
|
36
|
+
display: grid;
|
|
37
|
+
}
|
|
38
|
+
|
|
14
39
|
.table-skeleton__cell {
|
|
15
40
|
display: grid;
|
|
16
41
|
min-width: 0;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
export type ClientTableState = 'ready' | 'empty' | 'error';
|
|
2
|
+
|
|
3
|
+
export type ClientTablePage<TItem> = {
|
|
4
|
+
items: readonly TItem[];
|
|
5
|
+
nextCursor?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ClientTablePaginationController = {
|
|
9
|
+
destroy(): void;
|
|
10
|
+
remove(key: string): void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ClientTablePaginationOptions<TItem> = {
|
|
14
|
+
createRow(item: TItem): HTMLElement | null;
|
|
15
|
+
emptyMessage: string;
|
|
16
|
+
errorMessage: string;
|
|
17
|
+
getKey(item: TItem): string;
|
|
18
|
+
loadPage(cursor?: string): Promise<ClientTablePage<TItem>>;
|
|
19
|
+
root: HTMLElement;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function replaceClientTableRows(
|
|
23
|
+
root: HTMLElement,
|
|
24
|
+
rows: readonly HTMLElement[],
|
|
25
|
+
{
|
|
26
|
+
message,
|
|
27
|
+
state = rows.length > 0 ? 'ready' : 'empty',
|
|
28
|
+
}: {
|
|
29
|
+
message?: string;
|
|
30
|
+
state?: ClientTableState;
|
|
31
|
+
} = {},
|
|
32
|
+
): void {
|
|
33
|
+
const rowsRoot = root.querySelector<HTMLElement>('[data-table-rows]');
|
|
34
|
+
if (!rowsRoot) return;
|
|
35
|
+
|
|
36
|
+
rowsRoot
|
|
37
|
+
.querySelectorAll(
|
|
38
|
+
':scope > [data-table-row], :scope > [data-table-skeleton-row]',
|
|
39
|
+
)
|
|
40
|
+
.forEach((row) => row.remove());
|
|
41
|
+
|
|
42
|
+
const stateRow = rowsRoot.querySelector<HTMLElement>(
|
|
43
|
+
':scope > [data-table-state]',
|
|
44
|
+
);
|
|
45
|
+
if (stateRow) {
|
|
46
|
+
stateRow.hidden = state === 'ready';
|
|
47
|
+
stateRow.dataset.tableState = state === 'ready' ? 'empty' : state;
|
|
48
|
+
const content = stateRow.querySelector<HTMLElement>(
|
|
49
|
+
'[data-widget="text-block"]',
|
|
50
|
+
);
|
|
51
|
+
if (content && message !== undefined) content.textContent = message;
|
|
52
|
+
if (content) {
|
|
53
|
+
content.setAttribute('role', state === 'error' ? 'alert' : 'status');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (state === 'ready') {
|
|
58
|
+
const fragment = document.createDocumentFragment();
|
|
59
|
+
rows.forEach((row) => {
|
|
60
|
+
row.hidden = false;
|
|
61
|
+
fragment.append(row);
|
|
62
|
+
});
|
|
63
|
+
rowsRoot.insertBefore(fragment, stateRow);
|
|
64
|
+
root.dispatchEvent(
|
|
65
|
+
new CustomEvent('table:rows-appended', {
|
|
66
|
+
bubbles: true,
|
|
67
|
+
detail: { rows },
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
root.dataset.tableLoadingState = state;
|
|
73
|
+
root.removeAttribute('aria-busy');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createClientTablePagination<TItem>({
|
|
77
|
+
createRow,
|
|
78
|
+
emptyMessage,
|
|
79
|
+
errorMessage,
|
|
80
|
+
getKey,
|
|
81
|
+
loadPage,
|
|
82
|
+
root,
|
|
83
|
+
}: ClientTablePaginationOptions<TItem>): ClientTablePaginationController {
|
|
84
|
+
const rowsRoot = root.querySelector<HTMLElement>('[data-table-rows]');
|
|
85
|
+
const pagination = root.querySelector<HTMLElement>('[data-table-pagination]');
|
|
86
|
+
const sentinel = root.querySelector<HTMLElement>(
|
|
87
|
+
'[data-table-pagination-sentinel]',
|
|
88
|
+
);
|
|
89
|
+
const skeletonTemplate = root.querySelector<HTMLTemplateElement>(
|
|
90
|
+
'[data-table-skeleton-template]',
|
|
91
|
+
);
|
|
92
|
+
const keys = new Set<string>();
|
|
93
|
+
let nextCursor: string | undefined;
|
|
94
|
+
let loading = false;
|
|
95
|
+
let disposed = false;
|
|
96
|
+
|
|
97
|
+
if (pagination) {
|
|
98
|
+
pagination.hidden = false;
|
|
99
|
+
pagination.dataset.tableProgressiveEnhanced = 'true';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const setPaginationState = (state: 'idle' | 'loading' | 'error'): void => {
|
|
103
|
+
if (!pagination) return;
|
|
104
|
+
pagination.dataset.tableProgressiveState = state;
|
|
105
|
+
const status = pagination.querySelector<HTMLElement>(
|
|
106
|
+
'[data-table-pagination-status]',
|
|
107
|
+
);
|
|
108
|
+
if (!status) return;
|
|
109
|
+
status.textContent =
|
|
110
|
+
state === 'loading'
|
|
111
|
+
? (pagination.dataset.tableLoadingLabel ?? '')
|
|
112
|
+
: state === 'error'
|
|
113
|
+
? (pagination.dataset.tableErrorLabel ?? '')
|
|
114
|
+
: '';
|
|
115
|
+
status.hidden = state === 'idle';
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const appendSkeleton = (): HTMLElement[] => {
|
|
119
|
+
if (!rowsRoot || !skeletonTemplate) return [];
|
|
120
|
+
const skeletonRows = Array.from(
|
|
121
|
+
skeletonTemplate.content.querySelectorAll<HTMLElement>(
|
|
122
|
+
'[data-table-skeleton-row]',
|
|
123
|
+
),
|
|
124
|
+
).map((row) => row.cloneNode(true) as HTMLElement);
|
|
125
|
+
const stateRow = rowsRoot.querySelector<HTMLElement>(
|
|
126
|
+
':scope > [data-table-state]',
|
|
127
|
+
);
|
|
128
|
+
const fragment = document.createDocumentFragment();
|
|
129
|
+
skeletonRows.forEach((row) => fragment.append(row));
|
|
130
|
+
rowsRoot.insertBefore(fragment, stateRow);
|
|
131
|
+
return skeletonRows;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const createPageRows = (page: ClientTablePage<TItem>): HTMLElement[] => {
|
|
135
|
+
const rows: HTMLElement[] = [];
|
|
136
|
+
for (const item of page.items) {
|
|
137
|
+
const key = getKey(item);
|
|
138
|
+
if (keys.has(key)) continue;
|
|
139
|
+
const row = createRow(item);
|
|
140
|
+
if (!row) continue;
|
|
141
|
+
keys.add(key);
|
|
142
|
+
rows.push(row);
|
|
143
|
+
}
|
|
144
|
+
nextCursor = page.nextCursor;
|
|
145
|
+
return rows;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const appendRows = (rows: readonly HTMLElement[]): void => {
|
|
149
|
+
if (!rowsRoot) return;
|
|
150
|
+
const stateRow = rowsRoot.querySelector<HTMLElement>(
|
|
151
|
+
':scope > [data-table-state]',
|
|
152
|
+
);
|
|
153
|
+
const fragment = document.createDocumentFragment();
|
|
154
|
+
rows.forEach((row) => {
|
|
155
|
+
row.hidden = false;
|
|
156
|
+
fragment.append(row);
|
|
157
|
+
});
|
|
158
|
+
rowsRoot.insertBefore(fragment, stateRow);
|
|
159
|
+
root.dispatchEvent(
|
|
160
|
+
new CustomEvent('table:rows-appended', {
|
|
161
|
+
bubbles: true,
|
|
162
|
+
detail: { rows },
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
root.dataset.tableLoadingState = 'ready';
|
|
166
|
+
root.removeAttribute('aria-busy');
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const load = async (append: boolean): Promise<void> => {
|
|
170
|
+
if (loading || disposed || (append && !nextCursor)) return;
|
|
171
|
+
loading = true;
|
|
172
|
+
root.setAttribute('aria-busy', 'true');
|
|
173
|
+
root.dataset.tableLoadingState = append ? 'appending' : 'pending';
|
|
174
|
+
setPaginationState('loading');
|
|
175
|
+
const skeletonRows = append ? appendSkeleton() : [];
|
|
176
|
+
let shouldContinue = false;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const page = await loadPage(append ? nextCursor : undefined);
|
|
180
|
+
if (disposed) return;
|
|
181
|
+
const rows = createPageRows(page);
|
|
182
|
+
if (append) {
|
|
183
|
+
skeletonRows.forEach((row) => row.remove());
|
|
184
|
+
appendRows(rows);
|
|
185
|
+
} else {
|
|
186
|
+
replaceClientTableRows(root, rows, { message: emptyMessage });
|
|
187
|
+
}
|
|
188
|
+
setPaginationState('idle');
|
|
189
|
+
if (!nextCursor) observer?.disconnect();
|
|
190
|
+
shouldContinue = Boolean(
|
|
191
|
+
nextCursor &&
|
|
192
|
+
sentinel &&
|
|
193
|
+
sentinel.getBoundingClientRect().top <= window.innerHeight + 512,
|
|
194
|
+
);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
if (disposed) return;
|
|
197
|
+
console.error('[table] Failed to load client page', error);
|
|
198
|
+
if (append) {
|
|
199
|
+
skeletonRows.forEach((row) => row.remove());
|
|
200
|
+
root.removeAttribute('aria-busy');
|
|
201
|
+
root.dataset.tableLoadingState = 'ready';
|
|
202
|
+
setPaginationState('error');
|
|
203
|
+
} else {
|
|
204
|
+
replaceClientTableRows(root, [], {
|
|
205
|
+
message: errorMessage,
|
|
206
|
+
state: 'error',
|
|
207
|
+
});
|
|
208
|
+
setPaginationState('error');
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
loading = false;
|
|
212
|
+
if (shouldContinue && !disposed) {
|
|
213
|
+
window.requestAnimationFrame(() => void load(true));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const observer =
|
|
219
|
+
sentinel && 'IntersectionObserver' in window
|
|
220
|
+
? new IntersectionObserver(
|
|
221
|
+
(entries) => {
|
|
222
|
+
if (entries.some((entry) => entry.isIntersecting)) void load(true);
|
|
223
|
+
},
|
|
224
|
+
{ rootMargin: '512px 0px' },
|
|
225
|
+
)
|
|
226
|
+
: null;
|
|
227
|
+
|
|
228
|
+
const initialize = async (): Promise<void> => {
|
|
229
|
+
await load(false);
|
|
230
|
+
if (!disposed && nextCursor && sentinel && observer) {
|
|
231
|
+
observer.observe(sentinel);
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
void initialize();
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
destroy() {
|
|
239
|
+
disposed = true;
|
|
240
|
+
observer?.disconnect();
|
|
241
|
+
},
|
|
242
|
+
remove(key) {
|
|
243
|
+
if (!keys.delete(key)) return;
|
|
244
|
+
const row = Array.from(
|
|
245
|
+
root.querySelectorAll<HTMLElement>('[data-table-row]'),
|
|
246
|
+
).find((candidate) => candidate.dataset.tableRowKey === key);
|
|
247
|
+
row?.remove();
|
|
248
|
+
if (keys.size === 0) {
|
|
249
|
+
replaceClientTableRows(root, [], { message: emptyMessage });
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
@@ -4,6 +4,10 @@ import {
|
|
|
4
4
|
type ComboboxChangeDetail,
|
|
5
5
|
} from '../Combobox/comboboxController';
|
|
6
6
|
import { resetTableSort } from './tableSortController';
|
|
7
|
+
import {
|
|
8
|
+
hasTableProgressive,
|
|
9
|
+
setTableProgressiveFilters,
|
|
10
|
+
} from './tableProgressiveController';
|
|
7
11
|
|
|
8
12
|
type FilterValues = Readonly<Record<string, readonly string[]>>;
|
|
9
13
|
|
|
@@ -13,8 +17,57 @@ type CachedFilterValues = {
|
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
const initializedAttribute = 'data-table-filters-initialized';
|
|
20
|
+
const externalFiltersAttribute = 'data-table-filter-external';
|
|
21
|
+
const exclusionsAttribute = 'data-table-filter-exclusions';
|
|
22
|
+
const refreshEvent = 'table-filter:refresh';
|
|
16
23
|
const noop = (): void => undefined;
|
|
17
24
|
|
|
25
|
+
export function setTableExternalFilterActive(
|
|
26
|
+
root: HTMLElement,
|
|
27
|
+
key: string,
|
|
28
|
+
active: boolean,
|
|
29
|
+
): void {
|
|
30
|
+
setTokenAttribute(root, externalFiltersAttribute, key, active);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function setTableRowFilterExclusion(
|
|
34
|
+
row: HTMLElement,
|
|
35
|
+
key: string,
|
|
36
|
+
excluded: boolean,
|
|
37
|
+
): void {
|
|
38
|
+
setTokenAttribute(row, exclusionsAttribute, key, excluded);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setTokenAttribute(
|
|
42
|
+
element: HTMLElement,
|
|
43
|
+
attribute: string,
|
|
44
|
+
key: string,
|
|
45
|
+
enabled: boolean,
|
|
46
|
+
): void {
|
|
47
|
+
const normalizedKey = key.trim();
|
|
48
|
+
if (!normalizedKey || /\s/.test(normalizedKey)) {
|
|
49
|
+
throw new Error('Table filter keys must be non-empty tokens');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const tokens = new Set(
|
|
53
|
+
(element.getAttribute(attribute) ?? '')
|
|
54
|
+
.split(/\s+/)
|
|
55
|
+
.filter(Boolean),
|
|
56
|
+
);
|
|
57
|
+
if (enabled) tokens.add(normalizedKey);
|
|
58
|
+
else tokens.delete(normalizedKey);
|
|
59
|
+
|
|
60
|
+
if (tokens.size > 0) {
|
|
61
|
+
element.setAttribute(attribute, [...tokens].join(' '));
|
|
62
|
+
} else {
|
|
63
|
+
element.removeAttribute(attribute);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function refreshTableFilters(root: HTMLElement): void {
|
|
68
|
+
root.dispatchEvent(new Event(refreshEvent));
|
|
69
|
+
}
|
|
70
|
+
|
|
18
71
|
export function initTableFilters(root: HTMLElement): () => void {
|
|
19
72
|
if (root.getAttribute(initializedAttribute) === 'true') return noop;
|
|
20
73
|
|
|
@@ -55,23 +108,10 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
55
108
|
}
|
|
56
109
|
};
|
|
57
110
|
|
|
58
|
-
const isLoadingAllRows = (): boolean => {
|
|
59
|
-
const pagination = table.querySelector<HTMLElement>(
|
|
60
|
-
'[data-table-pagination]',
|
|
61
|
-
);
|
|
62
|
-
const state = pagination?.dataset.tableProgressiveState;
|
|
63
|
-
return Boolean(
|
|
64
|
-
pagination &&
|
|
65
|
-
!pagination.hidden &&
|
|
66
|
-
state !== 'complete' &&
|
|
67
|
-
state !== 'error',
|
|
68
|
-
);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
111
|
const updateEmptyState = (visibleRows: number, active: boolean): void => {
|
|
72
112
|
const empty = table.querySelector<HTMLElement>('[data-table-filter-empty]');
|
|
73
113
|
if (!empty) return;
|
|
74
|
-
empty.hidden = !active || visibleRows > 0
|
|
114
|
+
empty.hidden = !active || visibleRows > 0;
|
|
75
115
|
};
|
|
76
116
|
|
|
77
117
|
const applyFilters = (): void => {
|
|
@@ -79,7 +119,21 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
79
119
|
const activeSelections = [...selections.entries()].filter(
|
|
80
120
|
([, value]) => value.length > 0,
|
|
81
121
|
);
|
|
82
|
-
const
|
|
122
|
+
const hasExternalFilter =
|
|
123
|
+
root.hasAttribute(externalFiltersAttribute) ||
|
|
124
|
+
rows.some((row) => row.hasAttribute(exclusionsAttribute));
|
|
125
|
+
const active =
|
|
126
|
+
query.length > 0 || activeSelections.length > 0 || hasExternalFilter;
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
setTableProgressiveFilters(table, {
|
|
130
|
+
search: search?.value.trim() ?? '',
|
|
131
|
+
selections: Object.fromEntries(activeSelections),
|
|
132
|
+
})
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
83
137
|
let visibleRows = 0;
|
|
84
138
|
|
|
85
139
|
for (const row of rows) {
|
|
@@ -92,19 +146,25 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
92
146
|
([field, selectedValue]) =>
|
|
93
147
|
(cached.values[field] ?? []).includes(selectedValue),
|
|
94
148
|
);
|
|
95
|
-
const visible =
|
|
149
|
+
const visible =
|
|
150
|
+
matchesSearch &&
|
|
151
|
+
matchesSelections &&
|
|
152
|
+
!row.hasAttribute(exclusionsAttribute);
|
|
96
153
|
|
|
97
154
|
if (row.hidden !== !visible) row.hidden = !visible;
|
|
98
155
|
if (visible) visibleRows += 1;
|
|
99
156
|
}
|
|
100
|
-
|
|
101
157
|
updateEmptyState(visibleRows, active);
|
|
102
|
-
if (active && isLoadingAllRows()) {
|
|
103
|
-
table.dispatchEvent(new CustomEvent('table:load-all', { bubbles: true }));
|
|
104
|
-
}
|
|
105
158
|
};
|
|
106
159
|
|
|
107
160
|
const scheduleFilters = (): void => {
|
|
161
|
+
if (
|
|
162
|
+
table.dataset.tablePreparation === 'controlled' ||
|
|
163
|
+
hasTableProgressive(table)
|
|
164
|
+
) {
|
|
165
|
+
applyFilters();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
108
168
|
if (scheduledFrame !== null) return;
|
|
109
169
|
scheduledFrame = window.requestAnimationFrame(() => {
|
|
110
170
|
scheduledFrame = null;
|
|
@@ -136,11 +196,7 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
136
196
|
const detail = (event as CustomEvent<{ rows?: readonly HTMLElement[] }>)
|
|
137
197
|
.detail;
|
|
138
198
|
registerRows(detail?.rows ?? []);
|
|
139
|
-
scheduleFilters();
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const handleProgressiveComplete = (): void => {
|
|
143
|
-
scheduleFilters();
|
|
199
|
+
if (!hasTableProgressive(table)) scheduleFilters();
|
|
144
200
|
};
|
|
145
201
|
|
|
146
202
|
const handleReset = (): void => {
|
|
@@ -168,11 +224,8 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
168
224
|
applyFilters();
|
|
169
225
|
search?.addEventListener('input', handleSearch);
|
|
170
226
|
root.addEventListener('combobox:change', handleComboboxChange);
|
|
227
|
+
root.addEventListener(refreshEvent, scheduleFilters);
|
|
171
228
|
table.addEventListener('table:rows-appended', handleRowsAppended);
|
|
172
|
-
table.addEventListener(
|
|
173
|
-
'table:progressive-complete',
|
|
174
|
-
handleProgressiveComplete,
|
|
175
|
-
);
|
|
176
229
|
reset?.addEventListener('click', handleReset);
|
|
177
230
|
|
|
178
231
|
return () => {
|
|
@@ -182,11 +235,8 @@ export function initTableFilters(root: HTMLElement): () => void {
|
|
|
182
235
|
}
|
|
183
236
|
search?.removeEventListener('input', handleSearch);
|
|
184
237
|
root.removeEventListener('combobox:change', handleComboboxChange);
|
|
238
|
+
root.removeEventListener(refreshEvent, scheduleFilters);
|
|
185
239
|
table.removeEventListener('table:rows-appended', handleRowsAppended);
|
|
186
|
-
table.removeEventListener(
|
|
187
|
-
'table:progressive-complete',
|
|
188
|
-
handleProgressiveComplete,
|
|
189
|
-
);
|
|
190
240
|
reset?.removeEventListener('click', handleReset);
|
|
191
241
|
for (const row of rows) row.hidden = false;
|
|
192
242
|
updateEmptyState(0, false);
|