@vobs/table 1.0.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/LICENSE +21 -0
- package/README.md +57 -0
- package/package.json +33 -0
- package/src/column-settings.ts +381 -0
- package/src/data-table.test.ts +536 -0
- package/src/data-table.ts +686 -0
- package/src/index.ts +28 -0
- package/src/persistence.ts +99 -0
- package/src/styles/styles.css +1 -0
- package/src/styles/table.css +209 -0
- package/src/types.ts +183 -0
- package/src/utils.ts +172 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export { KitDataTable } from './data-table'
|
|
2
|
+
export { KitColumnSettings } from './column-settings'
|
|
3
|
+
export { createColumnSettingsPersistence, createLocalColumnSettingsPersistence } from './persistence'
|
|
4
|
+
export type {
|
|
5
|
+
DataTableAlign,
|
|
6
|
+
DataTableChildren,
|
|
7
|
+
DataTableColumn,
|
|
8
|
+
DataTableColumnSettings,
|
|
9
|
+
DataTableColumnSettingsPersistence,
|
|
10
|
+
DataTableColumnSettingsStorage,
|
|
11
|
+
DataTableCommonProps,
|
|
12
|
+
DataTableIcons,
|
|
13
|
+
DataTablePage,
|
|
14
|
+
DataTablePagination,
|
|
15
|
+
DataTablePaginationContext,
|
|
16
|
+
DataTablePaginationMode,
|
|
17
|
+
DataTableResource,
|
|
18
|
+
DataTableQuery,
|
|
19
|
+
DataTableResourceData,
|
|
20
|
+
DataTableSort,
|
|
21
|
+
DataTableSortDirection,
|
|
22
|
+
DataTableSortType,
|
|
23
|
+
DataTableSortingMode,
|
|
24
|
+
KitColumnSettingsProps,
|
|
25
|
+
KitDataTableProps
|
|
26
|
+
} from './types'
|
|
27
|
+
|
|
28
|
+
export { createDefaultColumnSettings, normalizeColumnSettings } from './utils'
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DataTableColumnSettings,
|
|
3
|
+
DataTableColumnSettingsPersistence,
|
|
4
|
+
DataTableColumnSettingsStorage
|
|
5
|
+
} from './types'
|
|
6
|
+
|
|
7
|
+
export function createColumnSettingsPersistence(
|
|
8
|
+
storage: DataTableColumnSettingsStorage,
|
|
9
|
+
key: string
|
|
10
|
+
): DataTableColumnSettingsPersistence {
|
|
11
|
+
const normalizedKey = key.trim()
|
|
12
|
+
if (!normalizedKey) throw new Error('VOBS_TABLE001: column settings key 不能为空')
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
load(): DataTableColumnSettings | null | undefined {
|
|
16
|
+
return storage.get<DataTableColumnSettings>(normalizedKey)
|
|
17
|
+
},
|
|
18
|
+
save(settings: DataTableColumnSettings): void {
|
|
19
|
+
storage.set(normalizedKey, settings)
|
|
20
|
+
},
|
|
21
|
+
reset(): void {
|
|
22
|
+
storage.remove(normalizedKey)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createLocalColumnSettingsPersistence(
|
|
28
|
+
key: string,
|
|
29
|
+
storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>
|
|
30
|
+
): DataTableColumnSettingsPersistence {
|
|
31
|
+
const normalizedKey = key.trim()
|
|
32
|
+
if (!normalizedKey) throw new Error('VOBS_TABLE001: column settings key 不能为空')
|
|
33
|
+
const memory = new Map<string, string>()
|
|
34
|
+
let backend = storage ?? resolveLocalStorage()
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
load(): DataTableColumnSettings | undefined {
|
|
38
|
+
const raw = read()
|
|
39
|
+
if (!raw) return undefined
|
|
40
|
+
try {
|
|
41
|
+
const value: unknown = JSON.parse(raw)
|
|
42
|
+
return isColumnSettings(value) ? value : undefined
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
save(settings: DataTableColumnSettings): void {
|
|
48
|
+
const raw = JSON.stringify(settings)
|
|
49
|
+
memory.set(normalizedKey, raw)
|
|
50
|
+
try {
|
|
51
|
+
backend?.setItem(normalizedKey, raw)
|
|
52
|
+
} catch {
|
|
53
|
+
backend = undefined
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
reset(): void {
|
|
57
|
+
memory.delete(normalizedKey)
|
|
58
|
+
try {
|
|
59
|
+
backend?.removeItem(normalizedKey)
|
|
60
|
+
} catch {
|
|
61
|
+
backend = undefined
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function read(): string | null {
|
|
67
|
+
try {
|
|
68
|
+
return backend?.getItem(normalizedKey) ?? memory.get(normalizedKey) ?? null
|
|
69
|
+
} catch {
|
|
70
|
+
backend = undefined
|
|
71
|
+
return memory.get(normalizedKey) ?? null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function resolveLocalStorage(): Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | undefined {
|
|
77
|
+
if (typeof window === 'undefined') return undefined
|
|
78
|
+
try {
|
|
79
|
+
return window.localStorage
|
|
80
|
+
} catch {
|
|
81
|
+
return undefined
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isColumnSettings(value: unknown): value is DataTableColumnSettings {
|
|
86
|
+
if (!value || typeof value !== 'object') return false
|
|
87
|
+
const candidate = value as {
|
|
88
|
+
visibleColumnIds?: unknown
|
|
89
|
+
columnOrder?: unknown
|
|
90
|
+
columnWidths?: unknown
|
|
91
|
+
}
|
|
92
|
+
return Array.isArray(candidate.visibleColumnIds)
|
|
93
|
+
&& candidate.visibleColumnIds.every(id => typeof id === 'string')
|
|
94
|
+
&& Array.isArray(candidate.columnOrder)
|
|
95
|
+
&& candidate.columnOrder.every(id => typeof id === 'string')
|
|
96
|
+
&& Boolean(candidate.columnWidths)
|
|
97
|
+
&& typeof candidate.columnWidths === 'object'
|
|
98
|
+
&& !Array.isArray(candidate.columnWidths)
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import './table.css';
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
.vobs-data-table {
|
|
2
|
+
background: var(--vobs-table-surface, var(--bg-base-secondary, #fff));
|
|
3
|
+
border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
4
|
+
border-radius: var(--vobs-table-radius, 4px);
|
|
5
|
+
color: var(--vobs-table-text, var(--text-default, #1f2328));
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.vobs-data-table__toolbar,
|
|
10
|
+
.vobs-data-table__footer {
|
|
11
|
+
padding: var(--vobs-table-space, 12px);
|
|
12
|
+
border-color: var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.vobs-data-table__toolbar { border-bottom: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de)); }
|
|
16
|
+
.vobs-data-table__footer { border-top: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de)); }
|
|
17
|
+
.vobs-data-table__viewport { width: 100%; overflow-x: auto; }
|
|
18
|
+
.vobs-data-table__table { width: 100%; border-collapse: collapse; table-layout: fixed; }
|
|
19
|
+
.vobs-data-table th,
|
|
20
|
+
.vobs-data-table td {
|
|
21
|
+
padding: var(--vobs-table-cell-padding, 10px 12px);
|
|
22
|
+
border-bottom: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
23
|
+
text-align: start;
|
|
24
|
+
vertical-align: middle;
|
|
25
|
+
}
|
|
26
|
+
.vobs-data-table th { color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); font-size: 0.85em; font-weight: 600; }
|
|
27
|
+
.vobs-data-table--sticky-header thead th {
|
|
28
|
+
position: sticky;
|
|
29
|
+
top: var(--vobs-table-sticky-top, 0px);
|
|
30
|
+
z-index: 2;
|
|
31
|
+
background: var(--vobs-table-header-surface, var(--vobs-table-surface, var(--bg-base-secondary, #fff)));
|
|
32
|
+
box-shadow: 0 1px 0 var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
33
|
+
}
|
|
34
|
+
.vobs-data-table tbody tr:last-child td { border-bottom: 0; }
|
|
35
|
+
.vobs-data-table [data-align="center"] { text-align: center; }
|
|
36
|
+
.vobs-data-table [data-align="end"] { text-align: end; font-variant-numeric: tabular-nums; }
|
|
37
|
+
.vobs-data-table tr[data-clickable="true"] { cursor: pointer; }
|
|
38
|
+
.vobs-data-table tr[data-clickable="true"]:hover { background: var(--vobs-table-row-hover, var(--bg-overlay-l2, rgba(0, 0, 0, .04))); }
|
|
39
|
+
.vobs-data-table__sort,
|
|
40
|
+
.vobs-data-table__page-button {
|
|
41
|
+
border: 0;
|
|
42
|
+
background: transparent;
|
|
43
|
+
color: inherit;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
font: inherit;
|
|
46
|
+
}
|
|
47
|
+
.vobs-data-table__sort { display: inline-flex; align-items: center; gap: 6px; padding: 0; }
|
|
48
|
+
.vobs-data-table__sort:hover,
|
|
49
|
+
.vobs-data-table__sort:focus-visible { color: var(--vobs-table-sort-active, var(--text-brand, #32F08C)); }
|
|
50
|
+
.vobs-data-table__sort-icon {
|
|
51
|
+
display: inline-flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
min-width: 1em;
|
|
55
|
+
color: var(--vobs-table-muted, var(--text-tertiary, #57606a));
|
|
56
|
+
}
|
|
57
|
+
.vobs-data-table th[data-sort-direction] .vobs-data-table__sort-icon {
|
|
58
|
+
color: var(--vobs-table-sort-active, var(--text-brand, #32F08C));
|
|
59
|
+
}
|
|
60
|
+
.vobs-data-table__state { padding: 32px 12px; color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); text-align: center; }
|
|
61
|
+
.vobs-data-table__spacer { padding: 0; border: 0; }
|
|
62
|
+
.vobs-data-table__footer-content,
|
|
63
|
+
.vobs-data-table__pagination { display: flex; align-items: center; gap: 8px; }
|
|
64
|
+
.vobs-data-table__footer-content { justify-content: space-between; flex-wrap: wrap; }
|
|
65
|
+
.vobs-data-table__summary {
|
|
66
|
+
color: var(--vobs-table-muted, var(--text-tertiary, #57606a));
|
|
67
|
+
font-size: 0.85em;
|
|
68
|
+
font-weight: 600;
|
|
69
|
+
white-space: nowrap;
|
|
70
|
+
}
|
|
71
|
+
.vobs-data-table__pagination { flex-wrap: wrap; justify-content: flex-end; }
|
|
72
|
+
.vobs-data-table__pagination-pages { display: inline-flex; align-items: center; gap: 4px; }
|
|
73
|
+
.vobs-data-table__page-button {
|
|
74
|
+
display: inline-flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: center;
|
|
77
|
+
border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
78
|
+
border-radius: 2px;
|
|
79
|
+
min-width: 24px;
|
|
80
|
+
height: 24px;
|
|
81
|
+
padding: 0 6px;
|
|
82
|
+
}
|
|
83
|
+
.vobs-data-table__page-button.is-active {
|
|
84
|
+
background: var(--vobs-table-active, var(--bg-overlay-l2, rgba(0, 0, 0, .06)));
|
|
85
|
+
border-color: var(--vobs-table-active-border, var(--border-neutral-l2, #afb8c1));
|
|
86
|
+
}
|
|
87
|
+
.vobs-data-table__page-button:disabled { cursor: not-allowed; opacity: .45; }
|
|
88
|
+
.vobs-data-table__page-ellipsis { min-width: 24px; text-align: center; color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); }
|
|
89
|
+
.vobs-data-table__pagination-jump { display: inline-flex; align-items: center; gap: 6px; }
|
|
90
|
+
.vobs-data-table__page-size { display: inline-flex; align-items: center; gap: 6px; margin-left: 4px; color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); font-size: 0.85em; font-weight: 600; white-space: nowrap; }
|
|
91
|
+
.vobs-data-table__page-size-select {
|
|
92
|
+
box-sizing: border-box;
|
|
93
|
+
height: 24px;
|
|
94
|
+
padding: 0 20px 0 6px;
|
|
95
|
+
color: inherit;
|
|
96
|
+
background: var(--bg-overlay-l1, transparent);
|
|
97
|
+
border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
98
|
+
border-radius: 2px;
|
|
99
|
+
font: inherit;
|
|
100
|
+
font-weight: 400;
|
|
101
|
+
}
|
|
102
|
+
.vobs-data-table__jump-label { display: inline-flex; align-items: center; gap: 6px; color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); font-size: 0.85em; font-weight: 600; }
|
|
103
|
+
.vobs-data-table__jump-input {
|
|
104
|
+
box-sizing: border-box;
|
|
105
|
+
width: 52px;
|
|
106
|
+
height: 24px;
|
|
107
|
+
padding: 0 5px;
|
|
108
|
+
color: inherit;
|
|
109
|
+
background: transparent;
|
|
110
|
+
border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
111
|
+
border-radius: 2px;
|
|
112
|
+
font: inherit;
|
|
113
|
+
font-weight: 400;
|
|
114
|
+
text-align: center;
|
|
115
|
+
}
|
|
116
|
+
.vobs-data-table__jump-submit { min-width: 32px; }
|
|
117
|
+
|
|
118
|
+
.vobs-column-settings { position: relative; display: inline-block; }
|
|
119
|
+
.vobs-column-settings__trigger { cursor: pointer; list-style: none; }
|
|
120
|
+
.vobs-column-settings__trigger::-webkit-details-marker { display: none; }
|
|
121
|
+
.vobs-column-settings__panel {
|
|
122
|
+
position: absolute;
|
|
123
|
+
z-index: 10;
|
|
124
|
+
right: 0;
|
|
125
|
+
min-width: 160px;
|
|
126
|
+
margin-top: 4px;
|
|
127
|
+
padding: 8px;
|
|
128
|
+
background: var(--vobs-table-surface, var(--bg-base-secondary, #fff));
|
|
129
|
+
border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
130
|
+
border-radius: var(--vobs-table-radius, 4px);
|
|
131
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, .16);
|
|
132
|
+
}
|
|
133
|
+
.vobs-column-settings__option { display: flex; align-items: center; gap: 8px; padding: 4px; white-space: nowrap; }
|
|
134
|
+
.vobs-column-settings__reset {
|
|
135
|
+
display: block;
|
|
136
|
+
width: 100%;
|
|
137
|
+
margin-bottom: 8px;
|
|
138
|
+
padding: 4px 6px;
|
|
139
|
+
color: var(--vobs-table-muted, var(--text-tertiary, #57606a));
|
|
140
|
+
background: transparent;
|
|
141
|
+
border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
142
|
+
border-radius: 2px;
|
|
143
|
+
cursor: pointer;
|
|
144
|
+
font: inherit;
|
|
145
|
+
text-align: start;
|
|
146
|
+
}
|
|
147
|
+
.vobs-column-settings__reset:hover,
|
|
148
|
+
.vobs-column-settings__move:hover:not(:disabled) {
|
|
149
|
+
color: var(--vobs-table-text, var(--text-default, #1f2328));
|
|
150
|
+
background: var(--vobs-table-row-hover, var(--bg-overlay-l2, rgba(0, 0, 0, .04)));
|
|
151
|
+
}
|
|
152
|
+
.vobs-column-settings__visibility,
|
|
153
|
+
.vobs-column-settings__layout {
|
|
154
|
+
display: grid;
|
|
155
|
+
gap: 2px;
|
|
156
|
+
}
|
|
157
|
+
.vobs-column-settings__layout {
|
|
158
|
+
margin-top: 8px;
|
|
159
|
+
padding-top: 8px;
|
|
160
|
+
border-top: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
161
|
+
}
|
|
162
|
+
.vobs-column-settings__layout-option {
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
justify-content: space-between;
|
|
166
|
+
gap: 12px;
|
|
167
|
+
padding: 4px;
|
|
168
|
+
}
|
|
169
|
+
.vobs-column-settings__layout-label {
|
|
170
|
+
min-width: 0;
|
|
171
|
+
overflow: hidden;
|
|
172
|
+
text-overflow: ellipsis;
|
|
173
|
+
white-space: nowrap;
|
|
174
|
+
}
|
|
175
|
+
.vobs-column-settings__layout-controls {
|
|
176
|
+
display: inline-flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 3px;
|
|
179
|
+
flex: 0 0 auto;
|
|
180
|
+
}
|
|
181
|
+
.vobs-column-settings__move {
|
|
182
|
+
display: inline-flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
justify-content: center;
|
|
185
|
+
width: 24px;
|
|
186
|
+
height: 24px;
|
|
187
|
+
padding: 0;
|
|
188
|
+
color: inherit;
|
|
189
|
+
background: transparent;
|
|
190
|
+
border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
191
|
+
border-radius: 2px;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
font: inherit;
|
|
194
|
+
line-height: 1;
|
|
195
|
+
}
|
|
196
|
+
.vobs-column-settings__move:disabled { cursor: not-allowed; opacity: .45; }
|
|
197
|
+
.vobs-column-settings__width {
|
|
198
|
+
box-sizing: border-box;
|
|
199
|
+
width: 64px;
|
|
200
|
+
height: 24px;
|
|
201
|
+
padding: 0 5px;
|
|
202
|
+
color: inherit;
|
|
203
|
+
background: transparent;
|
|
204
|
+
border: 1px solid var(--vobs-table-border, var(--border-neutral-l1, #d0d7de));
|
|
205
|
+
border-radius: 2px;
|
|
206
|
+
font: inherit;
|
|
207
|
+
text-align: end;
|
|
208
|
+
}
|
|
209
|
+
.vobs-column-settings__width::placeholder { color: var(--vobs-table-muted, var(--text-tertiary, #57606a)); }
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { VobsNode } from '@vobs/vobs'
|
|
2
|
+
|
|
3
|
+
export type DataTableAlign = 'start' | 'center' | 'end'
|
|
4
|
+
export type DataTableSortDirection = 'asc' | 'desc'
|
|
5
|
+
export type DataTableSortType = 'text' | 'number' | 'date' | 'boolean'
|
|
6
|
+
export type DataTableSortingMode = 'client' | 'server'
|
|
7
|
+
export type DataTablePaginationMode = 'simple' | 'standard' | 'all'
|
|
8
|
+
|
|
9
|
+
export interface DataTableSort {
|
|
10
|
+
readonly columnId: string
|
|
11
|
+
readonly direction: DataTableSortDirection
|
|
12
|
+
/** Optional backend field supplied by the column definition. */
|
|
13
|
+
readonly sortKey?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DataTableQuery {
|
|
17
|
+
readonly page: number
|
|
18
|
+
readonly pageSize: number
|
|
19
|
+
readonly sort: DataTableSort | null
|
|
20
|
+
readonly filters: Readonly<Record<string, unknown>>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface DataTablePage<Row> {
|
|
24
|
+
readonly rows: readonly Row[]
|
|
25
|
+
readonly total?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type DataTableResourceData<Row> = readonly Row[] | DataTablePage<Row>
|
|
29
|
+
|
|
30
|
+
export interface DataTableResource<Row> {
|
|
31
|
+
readonly data: { readonly value: DataTableResourceData<Row> | null }
|
|
32
|
+
readonly error: { readonly value: Error | null }
|
|
33
|
+
readonly loading: { readonly value: boolean }
|
|
34
|
+
refetch(): Promise<unknown>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type DataTableChildren =
|
|
38
|
+
| VobsNode
|
|
39
|
+
| string
|
|
40
|
+
| number
|
|
41
|
+
| readonly DataTableChildren[]
|
|
42
|
+
| (() => DataTableChildren | null | undefined)
|
|
43
|
+
|
|
44
|
+
export interface DataTableColumn<Row = Record<string, unknown>> {
|
|
45
|
+
readonly id: string
|
|
46
|
+
readonly label: string
|
|
47
|
+
readonly key?: keyof Row
|
|
48
|
+
readonly render?: (row: Row, index: number) => DataTableChildren
|
|
49
|
+
readonly compare?: (left: Row, right: Row) => number
|
|
50
|
+
/** Value used by the built-in client sorter when compare is not provided. */
|
|
51
|
+
readonly sortValue?: (row: Row) => unknown
|
|
52
|
+
/** Optional value kind for deterministic client-side sorting. */
|
|
53
|
+
readonly sortType?: DataTableSortType
|
|
54
|
+
/** Backend field mapping; emitted together with the stable column id in DataTableSort. */
|
|
55
|
+
readonly sortKey?: string
|
|
56
|
+
/** Apply a filter value supplied by the page or another external filter control. */
|
|
57
|
+
readonly filter?: (row: Row, value: unknown) => boolean
|
|
58
|
+
readonly sortable?: boolean
|
|
59
|
+
/** Marks a column as supported by an external filter UI; no control is rendered in the table. */
|
|
60
|
+
readonly filterable?: boolean
|
|
61
|
+
readonly visible?: boolean
|
|
62
|
+
readonly align?: DataTableAlign
|
|
63
|
+
readonly width?: string | number
|
|
64
|
+
readonly minWidth?: string | number
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DataTableColumnSettings {
|
|
68
|
+
readonly visibleColumnIds: readonly string[]
|
|
69
|
+
readonly columnOrder: readonly string[]
|
|
70
|
+
readonly columnWidths: Readonly<Record<string, string | number>>
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DataTableColumnSettingsPersistence {
|
|
74
|
+
load(): DataTableColumnSettings | null | undefined
|
|
75
|
+
save(settings: DataTableColumnSettings): void
|
|
76
|
+
reset?(): void
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Minimal storage shape accepted by createColumnSettingsPersistence. */
|
|
80
|
+
export interface DataTableColumnSettingsStorage {
|
|
81
|
+
get<T>(key: string): T | null | undefined
|
|
82
|
+
set<T>(key: string, value: T): void
|
|
83
|
+
remove(key: string): void
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface DataTableIcons {
|
|
87
|
+
readonly ascending?: DataTableChildren
|
|
88
|
+
readonly descending?: DataTableChildren
|
|
89
|
+
readonly unsorted?: DataTableChildren
|
|
90
|
+
readonly previous?: DataTableChildren
|
|
91
|
+
readonly next?: DataTableChildren
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DataTablePaginationContext {
|
|
95
|
+
readonly page: number
|
|
96
|
+
readonly pageCount: number
|
|
97
|
+
readonly pageSize: number
|
|
98
|
+
readonly pageSizeOptions: readonly number[]
|
|
99
|
+
readonly total: number
|
|
100
|
+
readonly visibleCount: number
|
|
101
|
+
readonly goToPage: (page: number) => void
|
|
102
|
+
readonly setPageSize: (pageSize: number) => void
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export type DataTablePagination =
|
|
106
|
+
| DataTableChildren
|
|
107
|
+
| ((context: DataTablePaginationContext) => DataTableChildren | null | undefined)
|
|
108
|
+
|
|
109
|
+
export interface DataTableCommonProps {
|
|
110
|
+
readonly class?: string
|
|
111
|
+
readonly className?: string
|
|
112
|
+
readonly style?: string
|
|
113
|
+
readonly id?: string
|
|
114
|
+
readonly title?: string
|
|
115
|
+
readonly role?: string
|
|
116
|
+
readonly tabIndex?: number
|
|
117
|
+
readonly [name: `aria-${string}`]: string | number | boolean | undefined
|
|
118
|
+
readonly [name: `data-${string}`]: string | number | boolean | undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface KitDataTableProps<Row = Record<string, unknown>> extends DataTableCommonProps {
|
|
122
|
+
readonly columns?: readonly DataTableColumn<Row>[]
|
|
123
|
+
readonly rows?: readonly Row[]
|
|
124
|
+
readonly resource?: DataTableResource<Row>
|
|
125
|
+
readonly rowKey?: (row: Row, index: number) => unknown
|
|
126
|
+
readonly page?: number
|
|
127
|
+
readonly pageSize?: number
|
|
128
|
+
readonly total?: number
|
|
129
|
+
readonly sort?: DataTableSort | null
|
|
130
|
+
/** Client sorts loaded rows; server leaves row order to the resource owner. */
|
|
131
|
+
readonly sortingMode?: DataTableSortingMode
|
|
132
|
+
/** Query state is owned by the caller when provided and is never edited by table-owned inputs. */
|
|
133
|
+
readonly filters?: Readonly<Record<string, unknown>>
|
|
134
|
+
readonly columnSettings?: DataTableColumnSettings
|
|
135
|
+
readonly visibleColumnIds?: readonly string[]
|
|
136
|
+
readonly virtual?: boolean
|
|
137
|
+
readonly stickyHeader?: boolean
|
|
138
|
+
readonly virtualHeight?: number
|
|
139
|
+
readonly rowHeight?: number
|
|
140
|
+
readonly overscan?: number
|
|
141
|
+
readonly loading?: DataTableChildren
|
|
142
|
+
readonly empty?: DataTableChildren
|
|
143
|
+
readonly error?: (error: Error, retry: () => Promise<unknown>) => DataTableChildren
|
|
144
|
+
readonly toolbar?: DataTableChildren
|
|
145
|
+
readonly footer?: DataTableChildren
|
|
146
|
+
readonly pagination?: DataTablePagination
|
|
147
|
+
readonly paginationMode?: DataTablePaginationMode
|
|
148
|
+
readonly pageSizeOptions?: readonly number[]
|
|
149
|
+
readonly pageSizeLabel?: string
|
|
150
|
+
readonly icons?: DataTableIcons
|
|
151
|
+
readonly previousLabel?: string
|
|
152
|
+
readonly nextLabel?: string
|
|
153
|
+
readonly jumpToLabel?: string
|
|
154
|
+
readonly jumpToPlaceholder?: string
|
|
155
|
+
readonly jumpToSubmitLabel?: string
|
|
156
|
+
/** Formats the footer summary. The last two arguments are visible row count and page size. */
|
|
157
|
+
readonly pageLabel?: (
|
|
158
|
+
page: number,
|
|
159
|
+
pageCount: number,
|
|
160
|
+
total: number,
|
|
161
|
+
visibleCount: number,
|
|
162
|
+
pageSize: number
|
|
163
|
+
) => string
|
|
164
|
+
/** Receives table-originated sort and pagination changes for a page-level query controller. */
|
|
165
|
+
readonly onQueryChange?: (query: DataTableQuery) => void
|
|
166
|
+
readonly onRowClick?: (row: Row, index: number) => void
|
|
167
|
+
readonly onVisibleColumnIdsChange?: (ids: readonly string[]) => void
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface KitColumnSettingsProps<Row = Record<string, unknown>> extends DataTableCommonProps {
|
|
171
|
+
readonly columns?: readonly DataTableColumn<Row>[]
|
|
172
|
+
readonly settings?: DataTableColumnSettings
|
|
173
|
+
readonly persistence?: DataTableColumnSettingsPersistence
|
|
174
|
+
readonly visibleColumnIds?: readonly string[]
|
|
175
|
+
readonly trigger?: DataTableChildren
|
|
176
|
+
readonly label?: string
|
|
177
|
+
readonly closeLabel?: string
|
|
178
|
+
readonly resetLabel?: string
|
|
179
|
+
readonly onChange?: (ids: readonly string[]) => void
|
|
180
|
+
readonly onSettingsChange?: (settings: DataTableColumnSettings) => void
|
|
181
|
+
readonly onReset?: () => void
|
|
182
|
+
readonly onPersistenceError?: (error: unknown) => void
|
|
183
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { effect } from '@vobs/reactivity'
|
|
2
|
+
import {
|
|
3
|
+
createFragment,
|
|
4
|
+
createText,
|
|
5
|
+
insertBefore,
|
|
6
|
+
setAttribute,
|
|
7
|
+
setTextContent,
|
|
8
|
+
type VobsNode
|
|
9
|
+
} from '@vobs/vobs'
|
|
10
|
+
import type { DataTableColumn, DataTableColumnSettings } from './types'
|
|
11
|
+
export function readProp<T>(props: object, name: string, fallback: T): T {
|
|
12
|
+
const value = Reflect.get(props, name)
|
|
13
|
+
return (value === undefined ? fallback : value) as T
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function hasProp(props: object, name: string): boolean {
|
|
17
|
+
return name in props
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function bindClassList(root: Element, props: object, classes: () => readonly (string | undefined)[]): void {
|
|
21
|
+
effect(() => {
|
|
22
|
+
const userClasses = [readString(props, 'class'), readString(props, 'className')]
|
|
23
|
+
setAttribute(root, 'class', [...classes(), ...userClasses].filter(Boolean).join(' '))
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function bindCommonAttributes(root: Element, props: object, skip: readonly string[]): void {
|
|
28
|
+
const ignored = new Set([...skip, 'class', 'className', 'style', 'children'])
|
|
29
|
+
effect(() => {
|
|
30
|
+
for (const name of Object.keys(props)) {
|
|
31
|
+
if (ignored.has(name) || name.startsWith('on')) continue
|
|
32
|
+
if (name !== 'id' && name !== 'title' && name !== 'role' && name !== 'tabIndex'
|
|
33
|
+
&& !name.startsWith('aria-') && !name.startsWith('data-')) continue
|
|
34
|
+
const value = Reflect.get(props, name)
|
|
35
|
+
if (value === undefined || value === null || value === false) removeAttribute(root, normalizeAttributeName(name))
|
|
36
|
+
else setAttribute(root, normalizeAttributeName(name), String(value))
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function bindStyle(root: Element, props: object, internal: () => string = () => ''): void {
|
|
42
|
+
effect(() => {
|
|
43
|
+
const value = [internal(), readString(props, 'style')].filter(Boolean).join('; ')
|
|
44
|
+
if (value) setAttribute(root, 'style', value)
|
|
45
|
+
else removeAttribute(root, 'style')
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function bindText(node: Text, read: () => unknown): void {
|
|
50
|
+
effect(() => setTextContent(node, String(read() ?? '')))
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveSlot(value: unknown): VobsNode | null {
|
|
54
|
+
const resolved = typeof value === 'function' ? value() : value
|
|
55
|
+
if (resolved === undefined || resolved === null) return null
|
|
56
|
+
if (typeof resolved === 'string' || typeof resolved === 'number') return createText(String(resolved))
|
|
57
|
+
if (Array.isArray(resolved)) {
|
|
58
|
+
return createFragment((parent, anchor) => {
|
|
59
|
+
for (const child of resolved) {
|
|
60
|
+
const node = resolveSlot(child)
|
|
61
|
+
if (node) insertBefore(parent, node, anchor)
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
return resolved as VobsNode
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function setOptionalAttribute(node: Element, name: string, value: unknown): void {
|
|
69
|
+
if (value === undefined || value === null || value === false || value === '') removeAttribute(node, name)
|
|
70
|
+
else setAttribute(node, name, String(value))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function normalizePixels(value: string | number | undefined): string | undefined {
|
|
74
|
+
if (value === undefined) return undefined
|
|
75
|
+
return typeof value === 'number' ? `${value}px` : value
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createDefaultColumnSettings<Row>(
|
|
79
|
+
columns: readonly DataTableColumn<Row>[],
|
|
80
|
+
visibleColumnIds?: readonly string[]
|
|
81
|
+
): DataTableColumnSettings {
|
|
82
|
+
const columnOrder = columns.map(column => column.id)
|
|
83
|
+
const requested = new Set(visibleColumnIds ?? columns
|
|
84
|
+
.filter(column => column.visible !== false)
|
|
85
|
+
.map(column => column.id))
|
|
86
|
+
const visible = columnOrder.filter(id => requested.has(id)
|
|
87
|
+
&& columns.some(column => column.id === id && column.visible !== false))
|
|
88
|
+
if (visible.length === 0) {
|
|
89
|
+
const fallback = columns.find(column => column.visible !== false)
|
|
90
|
+
if (fallback) visible.push(fallback.id)
|
|
91
|
+
}
|
|
92
|
+
return { visibleColumnIds: visible, columnOrder, columnWidths: {} }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeColumnSettings<Row>(
|
|
96
|
+
columns: readonly DataTableColumn<Row>[],
|
|
97
|
+
settings?: Partial<DataTableColumnSettings> | null,
|
|
98
|
+
visibleColumnIds?: readonly string[]
|
|
99
|
+
): DataTableColumnSettings {
|
|
100
|
+
const fallback = createDefaultColumnSettings(columns, visibleColumnIds)
|
|
101
|
+
const knownIds = new Set(fallback.columnOrder)
|
|
102
|
+
const configuredOrder = Array.isArray(settings?.columnOrder) ? settings.columnOrder : undefined
|
|
103
|
+
const order = uniqueKnownIds(configuredOrder, knownIds)
|
|
104
|
+
for (const id of fallback.columnOrder) {
|
|
105
|
+
if (!order.includes(id)) order.push(id)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const configuredVisible = Array.isArray(settings?.visibleColumnIds) ? settings.visibleColumnIds : undefined
|
|
109
|
+
const requestedVisible = new Set(configuredVisible ?? fallback.visibleColumnIds)
|
|
110
|
+
const visible = order.filter(id => requestedVisible.has(id)
|
|
111
|
+
&& columns.some(column => column.id === id && column.visible !== false))
|
|
112
|
+
if (visible.length === 0) {
|
|
113
|
+
const fallbackVisible = order.find(id => columns.some(column => column.id === id && column.visible !== false))
|
|
114
|
+
if (fallbackVisible) visible.push(fallbackVisible)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const columnWidths: Record<string, string | number> = {}
|
|
118
|
+
const configuredWidths = settings?.columnWidths && typeof settings.columnWidths === 'object'
|
|
119
|
+
&& !Array.isArray(settings.columnWidths) ? settings.columnWidths : {}
|
|
120
|
+
for (const [id, width] of Object.entries(configuredWidths)) {
|
|
121
|
+
if (knownIds.has(id) && isColumnWidthValue(width)) columnWidths[id] = width
|
|
122
|
+
}
|
|
123
|
+
return { visibleColumnIds: visible, columnOrder: order, columnWidths }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function orderColumns<Row>(
|
|
127
|
+
columns: readonly DataTableColumn<Row>[],
|
|
128
|
+
columnOrder?: readonly string[]
|
|
129
|
+
): readonly DataTableColumn<Row>[] {
|
|
130
|
+
if (!columnOrder || columnOrder.length === 0) return columns
|
|
131
|
+
const byId = new Map(columns.map(column => [column.id, column]))
|
|
132
|
+
const ordered: DataTableColumn<Row>[] = []
|
|
133
|
+
const seen = new Set<string>()
|
|
134
|
+
for (const id of columnOrder) {
|
|
135
|
+
const column = byId.get(id)
|
|
136
|
+
if (column && !seen.has(id)) {
|
|
137
|
+
ordered.push(column)
|
|
138
|
+
seen.add(id)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const column of columns) {
|
|
142
|
+
if (!seen.has(column.id)) ordered.push(column)
|
|
143
|
+
}
|
|
144
|
+
return ordered
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function isColumnWidthValue(value: unknown): value is string | number {
|
|
148
|
+
return typeof value === 'number'
|
|
149
|
+
? Number.isFinite(value) && value > 0
|
|
150
|
+
: typeof value === 'string' && value.trim().length > 0 && !/[;{}]/u.test(value)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function uniqueKnownIds(values: readonly string[] | undefined, knownIds: ReadonlySet<string>): string[] {
|
|
154
|
+
const result: string[] = []
|
|
155
|
+
for (const id of values ?? []) {
|
|
156
|
+
if (knownIds.has(id) && !result.includes(id)) result.push(id)
|
|
157
|
+
}
|
|
158
|
+
return result
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function readString(props: object, name: string): string | undefined {
|
|
162
|
+
const value = Reflect.get(props, name)
|
|
163
|
+
return typeof value === 'string' ? value : undefined
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function normalizeAttributeName(name: string): string {
|
|
167
|
+
return name === 'tabIndex' ? 'tabindex' : name
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function removeAttribute(node: Element, name: string): void {
|
|
171
|
+
node.removeAttribute(name)
|
|
172
|
+
}
|