@coreui/data-grid 0.1.0-rc.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 +3 -0
- package/README.md +37 -0
- package/dist/css/data-grid.css +444 -0
- package/dist/css/data-grid.css.map +1 -0
- package/dist/css/data-grid.min.css +5 -0
- package/dist/css/data-grid.min.css.map +1 -0
- package/dist/js/csv.cjs +68 -0
- package/dist/js/csv.cjs.map +1 -0
- package/dist/js/csv.mjs +63 -0
- package/dist/js/csv.mjs.map +1 -0
- package/dist/js/data-grid.esm.js +9618 -0
- package/dist/js/data-grid.esm.js.map +1 -0
- package/dist/js/data-grid.esm.min.js +8 -0
- package/dist/js/data-grid.esm.min.js.map +1 -0
- package/dist/js/data-grid.js +9635 -0
- package/dist/js/data-grid.js.map +1 -0
- package/dist/js/data-grid.min.js +8 -0
- package/dist/js/data-grid.min.js.map +1 -0
- package/dist/js/data-grid.mjs +9618 -0
- package/dist/js/data-grid.mjs.map +1 -0
- package/dist/js/filters.cjs +143 -0
- package/dist/js/filters.cjs.map +1 -0
- package/dist/js/filters.mjs +137 -0
- package/dist/js/filters.mjs.map +1 -0
- package/dist/js/labels.cjs +76 -0
- package/dist/js/labels.cjs.map +1 -0
- package/dist/js/labels.mjs +71 -0
- package/dist/js/labels.mjs.map +1 -0
- package/dist/types/base-component.d.ts +19 -0
- package/dist/types/data-grid.d.ts +128 -0
- package/dist/types/dom/event-handler.d.ts +11 -0
- package/dist/types/dom/manipulator.d.ts +9 -0
- package/dist/types/engine/active-cell.d.ts +32 -0
- package/dist/types/engine/data-source.d.ts +19 -0
- package/dist/types/engine/scheduler.d.ts +11 -0
- package/dist/types/engine/table.d.ts +9 -0
- package/dist/types/engine/virtualizer.d.ts +21 -0
- package/dist/types/filters.d.mts +1 -0
- package/dist/types/filters.d.ts +13 -0
- package/dist/types/icons.d.ts +21 -0
- package/dist/types/index.d.mts +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/labels.d.mts +1 -0
- package/dist/types/labels.d.ts +9 -0
- package/dist/types/types.d.ts +262 -0
- package/dist/types/ui/body.d.ts +34 -0
- package/dist/types/ui/column-chooser.d.ts +27 -0
- package/dist/types/ui/editor.d.ts +8 -0
- package/dist/types/ui/filter-menu.d.ts +21 -0
- package/dist/types/ui/header.d.ts +50 -0
- package/dist/types/ui/loading.d.ts +12 -0
- package/dist/types/ui/menu.d.ts +17 -0
- package/dist/types/ui/pagination.d.ts +17 -0
- package/dist/types/ui/pinning.d.ts +17 -0
- package/dist/types/ui/toolbar.d.ts +34 -0
- package/dist/types/ui/tooltip.d.ts +10 -0
- package/dist/types/util/config.d.ts +11 -0
- package/dist/types/util/csv.d.mts +1 -0
- package/dist/types/util/csv.d.ts +19 -0
- package/dist/types/util/debounce.d.ts +7 -0
- package/dist/types/util/index.d.ts +10 -0
- package/dist/types/util/sanitizer.d.ts +11 -0
- package/package.json +147 -0
- package/scss/data-grid.scss +559 -0
- package/src/base-component.d.ts +19 -0
- package/src/base-component.js +90 -0
- package/src/data-grid.ts +2208 -0
- package/src/dom/data.js +59 -0
- package/src/dom/event-handler.d.ts +11 -0
- package/src/dom/event-handler.js +331 -0
- package/src/dom/manipulator.d.ts +9 -0
- package/src/dom/manipulator.js +75 -0
- package/src/engine/active-cell.ts +195 -0
- package/src/engine/data-source.ts +63 -0
- package/src/engine/scheduler.ts +42 -0
- package/src/engine/table.ts +122 -0
- package/src/engine/virtualizer.ts +57 -0
- package/src/filters.ts +176 -0
- package/src/icons.ts +60 -0
- package/src/index.ts +56 -0
- package/src/labels.ts +70 -0
- package/src/types.ts +334 -0
- package/src/ui/body.ts +348 -0
- package/src/ui/column-chooser.ts +183 -0
- package/src/ui/editor.ts +98 -0
- package/src/ui/filter-menu.ts +398 -0
- package/src/ui/header.ts +441 -0
- package/src/ui/loading.ts +39 -0
- package/src/ui/menu.ts +218 -0
- package/src/ui/pagination.ts +150 -0
- package/src/ui/pinning.ts +53 -0
- package/src/ui/toolbar.ts +144 -0
- package/src/ui/tooltip.ts +113 -0
- package/src/util/config.d.ts +11 -0
- package/src/util/config.js +69 -0
- package/src/util/csv.ts +85 -0
- package/src/util/debounce.ts +18 -0
- package/src/util/index.d.ts +10 -0
- package/src/util/index.js +310 -0
- package/src/util/sanitizer.d.ts +11 -0
- package/src/util/sanitizer.js +162 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid types.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Column, ColumnFiltersState, ColumnOrderState, ColumnPinningState, ColumnSizingState, PaginationState,
|
|
10
|
+
RowSelectionState, SortingState, Table, VisibilityState
|
|
11
|
+
} from '@tanstack/table-core'
|
|
12
|
+
import type { CsvDownloadOptions } from './util/csv'
|
|
13
|
+
|
|
14
|
+
export type {
|
|
15
|
+
Column,
|
|
16
|
+
ColumnFiltersState,
|
|
17
|
+
ColumnOrderState,
|
|
18
|
+
ColumnPinningState,
|
|
19
|
+
ColumnSizingState,
|
|
20
|
+
PaginationState,
|
|
21
|
+
RowSelectionState,
|
|
22
|
+
SortingState,
|
|
23
|
+
Table,
|
|
24
|
+
VisibilityState
|
|
25
|
+
} from '@tanstack/table-core'
|
|
26
|
+
|
|
27
|
+
export type DataGridItem = Record<string, unknown>
|
|
28
|
+
|
|
29
|
+
export interface DataGridColumnFilterContext {
|
|
30
|
+
column: Column<DataGridItem, unknown>
|
|
31
|
+
table: Table<DataGridItem>
|
|
32
|
+
labels: DataGridLabels
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type DataGridColumnFilterFactory = (context: DataGridColumnFilterContext) => DataGridSlot
|
|
36
|
+
|
|
37
|
+
export type DataGridFilterType = 'text' | 'number' | 'date' | 'select'
|
|
38
|
+
|
|
39
|
+
export type DataGridFilterOperator =
|
|
40
|
+
| 'contains'
|
|
41
|
+
| 'notContains'
|
|
42
|
+
| 'equals'
|
|
43
|
+
| 'notEquals'
|
|
44
|
+
| 'startsWith'
|
|
45
|
+
| 'endsWith'
|
|
46
|
+
| 'blank'
|
|
47
|
+
| 'notBlank'
|
|
48
|
+
| 'greaterThan'
|
|
49
|
+
| 'greaterThanOrEqual'
|
|
50
|
+
| 'lessThan'
|
|
51
|
+
| 'lessThanOrEqual'
|
|
52
|
+
| 'between'
|
|
53
|
+
|
|
54
|
+
export interface DataGridFilterCondition {
|
|
55
|
+
operator: DataGridFilterOperator
|
|
56
|
+
value?: unknown
|
|
57
|
+
// 'between' upper bound.
|
|
58
|
+
value2?: unknown
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The shape carried inside the TanStack columnFilters state: a plain string
|
|
62
|
+
// stays "text contains" (the quick input), the menu writes the structured
|
|
63
|
+
// forms.
|
|
64
|
+
export type DataGridColumnFilterValue =
|
|
65
|
+
| string
|
|
66
|
+
| { conditions: DataGridFilterCondition[]; join: 'and' | 'or' }
|
|
67
|
+
| { operator: 'in'; value: unknown[] }
|
|
68
|
+
|
|
69
|
+
export type DataGridEditorType = 'text' | 'number' | 'select'
|
|
70
|
+
|
|
71
|
+
export interface DataGridEditableOptions {
|
|
72
|
+
type?: DataGridEditorType
|
|
73
|
+
// number editor bounds/step.
|
|
74
|
+
min?: number
|
|
75
|
+
max?: number
|
|
76
|
+
step?: number
|
|
77
|
+
// select editor options: primitives or explicit label/value pairs.
|
|
78
|
+
options?: Array<string | number | { label: string; value: unknown }>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DataGridEditorContext {
|
|
82
|
+
item: DataGridItem
|
|
83
|
+
column: DataGridColumn
|
|
84
|
+
value: unknown
|
|
85
|
+
commit: (value: unknown) => void
|
|
86
|
+
cancel: () => void
|
|
87
|
+
labels: DataGridLabels
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The editor contract. Inline editors render inside the cell; popup: true
|
|
91
|
+
// renders into an overlay anchored to the cell (rich editors whose UI extends
|
|
92
|
+
// beyond the cell - date pickers, autocompletes, multi-selects). getValue
|
|
93
|
+
// feeds Enter/Tab/blur commits; contains() extends the edit scope to overlays
|
|
94
|
+
// portaled outside the editor element.
|
|
95
|
+
export interface DataGridEditor {
|
|
96
|
+
element: HTMLElement
|
|
97
|
+
contains?: (target: Node) => boolean
|
|
98
|
+
dispose?: () => void
|
|
99
|
+
focus?: () => void
|
|
100
|
+
getValue?: () => unknown
|
|
101
|
+
popup?: boolean
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type DataGridEditorFactory = (context: DataGridEditorContext) => DataGridEditor
|
|
105
|
+
|
|
106
|
+
// The editCommit payload. The grid never mutates items - the app applies the
|
|
107
|
+
// change and stays the single source of truth.
|
|
108
|
+
export interface DataGridEditCommitEvent {
|
|
109
|
+
item: DataGridItem
|
|
110
|
+
columnId: string
|
|
111
|
+
value: unknown
|
|
112
|
+
previousValue: unknown
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface DataGridColumn {
|
|
116
|
+
key: string
|
|
117
|
+
editable?: boolean | DataGridEditableOptions
|
|
118
|
+
editor?: DataGridEditorFactory
|
|
119
|
+
editValidate?: (value: unknown, item: DataGridItem) => true | string
|
|
120
|
+
filter?: DataGridColumnFilterFactory
|
|
121
|
+
filterable?: boolean
|
|
122
|
+
filterFn?: (value: unknown, filterValue: unknown, item: DataGridItem) => boolean
|
|
123
|
+
filterType?: DataGridFilterType
|
|
124
|
+
formatter?: (value: unknown, item: DataGridItem) => string
|
|
125
|
+
hideable?: boolean
|
|
126
|
+
label?: string
|
|
127
|
+
movable?: boolean
|
|
128
|
+
render?: (item: DataGridItem, index: number) => Node | string
|
|
129
|
+
resizable?: boolean
|
|
130
|
+
sortable?: boolean
|
|
131
|
+
style?: Partial<CSSStyleDeclaration>
|
|
132
|
+
width?: number
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface DataGridMenuAction {
|
|
136
|
+
key: string
|
|
137
|
+
label: string
|
|
138
|
+
disabled?: boolean
|
|
139
|
+
group?: string
|
|
140
|
+
icon?: string
|
|
141
|
+
run: () => void
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface DataGridColumnMenuContext {
|
|
145
|
+
column: DataGridColumn
|
|
146
|
+
defaultActions: DataGridMenuAction[]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type DataGridColumnMenuBuilder = (context: DataGridColumnMenuContext) => DataGridMenuAction[]
|
|
150
|
+
|
|
151
|
+
export interface DataGridLabels {
|
|
152
|
+
addCondition: string
|
|
153
|
+
applyFilter: string
|
|
154
|
+
clearFilter: string
|
|
155
|
+
clearSort: string
|
|
156
|
+
columnMenu: string
|
|
157
|
+
filterAction: string
|
|
158
|
+
filterColumn: string
|
|
159
|
+
firstPage: string
|
|
160
|
+
globalFilterLabel: string
|
|
161
|
+
globalFilterPlaceholder: string
|
|
162
|
+
hideColumn: string
|
|
163
|
+
itemsInfo: string
|
|
164
|
+
joinAnd: string
|
|
165
|
+
joinOr: string
|
|
166
|
+
lastPage: string
|
|
167
|
+
loadError: string
|
|
168
|
+
loading: string
|
|
169
|
+
moveLeft: string
|
|
170
|
+
moveRight: string
|
|
171
|
+
nextPage: string
|
|
172
|
+
operatorBetween: string
|
|
173
|
+
operatorBlank: string
|
|
174
|
+
operatorContains: string
|
|
175
|
+
operatorEndsWith: string
|
|
176
|
+
operatorEquals: string
|
|
177
|
+
operatorGreaterThan: string
|
|
178
|
+
operatorGreaterThanOrEqual: string
|
|
179
|
+
operatorLessThan: string
|
|
180
|
+
operatorLessThanOrEqual: string
|
|
181
|
+
operatorNotBlank: string
|
|
182
|
+
operatorNotContains: string
|
|
183
|
+
operatorNotEquals: string
|
|
184
|
+
operatorStartsWith: string
|
|
185
|
+
pageSizeLabel: string
|
|
186
|
+
paginationLabel: string
|
|
187
|
+
pinLeft: string
|
|
188
|
+
pinRight: string
|
|
189
|
+
previousPage: string
|
|
190
|
+
redoneAnnouncement: string
|
|
191
|
+
resetColumns: string
|
|
192
|
+
resultsAnnouncement: string
|
|
193
|
+
searchValues: string
|
|
194
|
+
selectAllRows: string
|
|
195
|
+
selectAllValues: string
|
|
196
|
+
selectRow: string
|
|
197
|
+
showAllColumns: string
|
|
198
|
+
sortAscending: string
|
|
199
|
+
sortDescending: string
|
|
200
|
+
toolbarColumns: string
|
|
201
|
+
toolbarExport: string
|
|
202
|
+
toolbarRedo: string
|
|
203
|
+
toolbarUndo: string
|
|
204
|
+
undoneAnnouncement: string
|
|
205
|
+
unpin: string
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface DataGridColumnSizingOptions {
|
|
209
|
+
mode?: 'onChange' | 'onEnd'
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface DataGridSlotContext {
|
|
213
|
+
table: Table<DataGridItem>
|
|
214
|
+
labels: DataGridLabels
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface DataGridSlot {
|
|
218
|
+
element: HTMLElement
|
|
219
|
+
update?: () => void
|
|
220
|
+
dispose?: () => void
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export type DataGridSlotFactory = (context: DataGridSlotContext) => DataGridSlot
|
|
224
|
+
|
|
225
|
+
export interface DataGridSlots {
|
|
226
|
+
toolbar: DataGridSlotFactory
|
|
227
|
+
pagination: DataGridSlotFactory
|
|
228
|
+
empty: DataGridSlotFactory
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface DataGridToolbarOptions {
|
|
232
|
+
columns?: boolean
|
|
233
|
+
export?: boolean | CsvDownloadOptions
|
|
234
|
+
history?: boolean
|
|
235
|
+
search?: boolean
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface DataGridSortingOptions {
|
|
239
|
+
multiple?: boolean
|
|
240
|
+
resetable?: boolean
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export type DataGridSorterVisibility = 'always' | 'hover' | false
|
|
244
|
+
|
|
245
|
+
export interface DataGridRowSelectionOptions {
|
|
246
|
+
selectAll?: boolean
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface DataGridPaginationInfoState {
|
|
250
|
+
pageCount: number
|
|
251
|
+
pageIndex: number
|
|
252
|
+
pageSize: number
|
|
253
|
+
totalRows: number
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export interface DataGridPaginationOptions {
|
|
257
|
+
info?: boolean | ((state: DataGridPaginationInfoState) => string)
|
|
258
|
+
pageSize?: number
|
|
259
|
+
pageSizeOptions?: number[]
|
|
260
|
+
position?: 'top' | 'bottom' | 'both'
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The serializable grid state snapshot: every user-adjustable TanStack slice.
|
|
264
|
+
// getState() returns it, restoreState() applies it, and `stateKey` persists
|
|
265
|
+
// it to localStorage (scroll position and edit drafts are not state).
|
|
266
|
+
export interface DataGridState {
|
|
267
|
+
columnFilters: ColumnFiltersState
|
|
268
|
+
columnOrder: ColumnOrderState
|
|
269
|
+
columnPinning: ColumnPinningState
|
|
270
|
+
columnSizing: ColumnSizingState
|
|
271
|
+
columnVisibility: VisibilityState
|
|
272
|
+
globalFilter: string
|
|
273
|
+
pagination: PaginationState
|
|
274
|
+
rowSelection: RowSelectionState
|
|
275
|
+
sorting: SortingState
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface DataGridDataRequest {
|
|
279
|
+
columnFilters: ColumnFiltersState
|
|
280
|
+
globalFilter: string
|
|
281
|
+
pagination: PaginationState
|
|
282
|
+
sorting: SortingState
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface DataGridDataResponse {
|
|
286
|
+
items: DataGridItem[]
|
|
287
|
+
totalRows: number
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export interface DataGridOptions {
|
|
291
|
+
cellNavigation: boolean
|
|
292
|
+
columnFilters: boolean
|
|
293
|
+
columnMenu: boolean | DataGridColumnMenuBuilder
|
|
294
|
+
columnMenuIcon: string
|
|
295
|
+
columnOrder: boolean | ColumnOrderState
|
|
296
|
+
columnPinning: boolean | ColumnPinningState
|
|
297
|
+
columnSizing: boolean | DataGridColumnSizingOptions
|
|
298
|
+
columnVisibility: boolean | VisibilityState
|
|
299
|
+
columns: DataGridColumn[]
|
|
300
|
+
dataProvider: ((request: DataGridDataRequest) => Promise<DataGridDataResponse>) | null
|
|
301
|
+
editing: boolean
|
|
302
|
+
empty: string | (() => Node | string)
|
|
303
|
+
filterIcon: string
|
|
304
|
+
globalFilter: boolean
|
|
305
|
+
hideColumnIcon: string
|
|
306
|
+
history: boolean
|
|
307
|
+
itemKey: ((item: DataGridItem, index: number) => string) | null
|
|
308
|
+
items: DataGridItem[]
|
|
309
|
+
labels: Partial<DataGridLabels>
|
|
310
|
+
moveLeftIcon: string
|
|
311
|
+
moveRightIcon: string
|
|
312
|
+
overscan: number
|
|
313
|
+
pagination: boolean | DataGridPaginationOptions
|
|
314
|
+
pinLeftIcon: string
|
|
315
|
+
pinRightIcon: string
|
|
316
|
+
rowHeight: number
|
|
317
|
+
rowSelection: boolean | DataGridRowSelectionOptions
|
|
318
|
+
sanitize: boolean
|
|
319
|
+
sanitizeFn: ((html: string) => string) | null
|
|
320
|
+
slots: Partial<DataGridSlots>
|
|
321
|
+
sortAscendingIcon: string
|
|
322
|
+
sortDescendingIcon: string
|
|
323
|
+
sortNeutralIcon: string
|
|
324
|
+
sorterVisibility: DataGridSorterVisibility
|
|
325
|
+
sorting: boolean | DataGridSortingOptions
|
|
326
|
+
stateKey: string | null
|
|
327
|
+
toolbar: boolean | DataGridToolbarOptions
|
|
328
|
+
toolbarColumnsIcon: string
|
|
329
|
+
toolbarExportIcon: string
|
|
330
|
+
toolbarRedoIcon: string
|
|
331
|
+
toolbarUndoIcon: string
|
|
332
|
+
unpinIcon: string
|
|
333
|
+
virtualization: boolean
|
|
334
|
+
}
|
package/src/ui/body.ts
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid ui/body.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Row } from '@tanstack/table-core'
|
|
9
|
+
import type { GridVirtualizerInstance } from '../engine/virtualizer'
|
|
10
|
+
import type { DataGridColumn, DataGridItem, DataGridLabels } from '../types'
|
|
11
|
+
import { applyPinnedCell, type GridPinning } from './pinning'
|
|
12
|
+
|
|
13
|
+
const CLASS_NAME_EMPTY = 'data-grid-empty'
|
|
14
|
+
const FOCUSABLE_SELECTOR = 'a[href], button, input, select, textarea, [tabindex]'
|
|
15
|
+
const CLASS_NAME_ROW_ODD = 'data-grid-row-odd'
|
|
16
|
+
const CLASS_NAME_ROW_SELECTED = 'table-active'
|
|
17
|
+
const CLASS_NAME_SELECT_CELL = 'data-grid-select-cell'
|
|
18
|
+
const CLASS_NAME_SPACER = 'data-grid-spacer'
|
|
19
|
+
|
|
20
|
+
export interface GridBodyContext {
|
|
21
|
+
// With cellNavigation on, rows/cells carry grid roles, every cell (and the
|
|
22
|
+
// interactive content inside it) drops out of the tab order, and cells are
|
|
23
|
+
// addressable by data-coreui-column - the roving tabindex pass in the grid
|
|
24
|
+
// root owns which single cell is tabbable.
|
|
25
|
+
cellNavigation: boolean
|
|
26
|
+
headerRowCount: number
|
|
27
|
+
labels: DataGridLabels
|
|
28
|
+
getDataVersion: () => number
|
|
29
|
+
// Inline editing: the cell being edited (keyed by rowId + columnId, NOT
|
|
30
|
+
// index - recycling changes indexes) renders the editor element instead of
|
|
31
|
+
// its content until commit/cancel. Null when editing is off or idle.
|
|
32
|
+
getEditingCell: () => { rowId: string; columnId: string } | null
|
|
33
|
+
getEditorElement: () => HTMLElement | null
|
|
34
|
+
getPinning: () => GridPinning | null
|
|
35
|
+
// Absolute index of the page's first row (pagination mode), else 0 - keeps
|
|
36
|
+
// aria-rowindex absolute like aria-rowcount.
|
|
37
|
+
getRowIndexOffset: () => number
|
|
38
|
+
getRows: () => Array<Row<DataGridItem>>
|
|
39
|
+
// Visible leaf columns in table order (pinning + columnOrder + visibility).
|
|
40
|
+
getVisibleColumns: () => DataGridColumn[]
|
|
41
|
+
getVirtualizer: () => GridVirtualizerInstance | null
|
|
42
|
+
renderEmpty: () => Node | string
|
|
43
|
+
// Guards every string injected via innerHTML (render results, empty state).
|
|
44
|
+
sanitizeContent: (html: string) => string
|
|
45
|
+
selection: boolean
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GridBody {
|
|
49
|
+
element: HTMLTableSectionElement
|
|
50
|
+
update: () => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const setCellContent = (
|
|
54
|
+
cellElement: HTMLTableCellElement,
|
|
55
|
+
content: Node | string,
|
|
56
|
+
sanitizeContent: (html: string) => string
|
|
57
|
+
): void => {
|
|
58
|
+
if (typeof content === 'string') {
|
|
59
|
+
cellElement.innerHTML = sanitizeContent(content)
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Re-inserting the same node detaches it (losing focus/selection in a custom
|
|
64
|
+
// empty slot); skip when it is already the sole child.
|
|
65
|
+
if (cellElement.childNodes.length === 1 && cellElement.firstChild === content) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
cellElement.replaceChildren(content)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const createBody = (context: GridBodyContext): GridBody => {
|
|
73
|
+
const element = document.createElement('tbody')
|
|
74
|
+
|
|
75
|
+
// Snapshot of the current layout, refreshed once per update() — cells are
|
|
76
|
+
// rebuilt (not recycled) when the visible column set or order changes.
|
|
77
|
+
let currentColumns: DataGridColumn[] = context.getVisibleColumns()
|
|
78
|
+
let layoutSignature = currentColumns.map(column => column.key).join('|')
|
|
79
|
+
let cellCount = currentColumns.length + (context.selection ? 1 : 0)
|
|
80
|
+
|
|
81
|
+
const buildRowCells = (rowElement: HTMLTableRowElement): void => {
|
|
82
|
+
rowElement.replaceChildren()
|
|
83
|
+
|
|
84
|
+
if (context.cellNavigation) {
|
|
85
|
+
rowElement.setAttribute('role', 'row')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const offset = context.selection ? 1 : 0
|
|
89
|
+
|
|
90
|
+
if (context.selection) {
|
|
91
|
+
const cellElement = document.createElement('td')
|
|
92
|
+
cellElement.classList.add(CLASS_NAME_SELECT_CELL)
|
|
93
|
+
const input = document.createElement('input')
|
|
94
|
+
input.type = 'checkbox'
|
|
95
|
+
input.classList.add('form-check-input')
|
|
96
|
+
input.setAttribute('aria-label', context.labels.selectRow)
|
|
97
|
+
input.dataset.coreuiRowSelect = ''
|
|
98
|
+
if (context.cellNavigation) {
|
|
99
|
+
cellElement.setAttribute('role', 'gridcell')
|
|
100
|
+
cellElement.setAttribute('aria-colindex', '1')
|
|
101
|
+
input.tabIndex = -1
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
cellElement.append(input)
|
|
105
|
+
rowElement.append(cellElement)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const [index, column] of currentColumns.entries()) {
|
|
109
|
+
const cellElement = document.createElement('td')
|
|
110
|
+
if (context.cellNavigation) {
|
|
111
|
+
cellElement.setAttribute('role', 'gridcell')
|
|
112
|
+
cellElement.setAttribute('aria-colindex', String(offset + index + 1))
|
|
113
|
+
cellElement.dataset.coreuiColumn = column.key
|
|
114
|
+
cellElement.tabIndex = -1
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
rowElement.append(cellElement)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
rowElement.dataset.coreuiLayout = layoutSignature
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const createRowElement = (): HTMLTableRowElement => {
|
|
124
|
+
const rowElement = document.createElement('tr')
|
|
125
|
+
buildRowCells(rowElement)
|
|
126
|
+
return rowElement
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const updateRowCells = (rowElement: HTMLTableRowElement, row: Row<DataGridItem>, index: number): void => {
|
|
130
|
+
const offset = context.selection ? 1 : 0
|
|
131
|
+
const editingCell = context.getEditingCell()
|
|
132
|
+
|
|
133
|
+
for (const [cellIndex, column] of currentColumns.entries()) {
|
|
134
|
+
const cellElement = rowElement.children[cellIndex + offset] as HTMLTableCellElement
|
|
135
|
+
|
|
136
|
+
if (editingCell && editingCell.rowId === row.id && editingCell.columnId === column.key) {
|
|
137
|
+
const editorElement = context.getEditorElement()
|
|
138
|
+
if (editorElement) {
|
|
139
|
+
cellElement.replaceChildren(editorElement)
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (column.render) {
|
|
145
|
+
setCellContent(cellElement, column.render(row.original, index), context.sanitizeContent)
|
|
146
|
+
if (context.cellNavigation) {
|
|
147
|
+
for (const focusable of cellElement.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)) {
|
|
148
|
+
focusable.tabIndex = -1
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
continue
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const value = row.getValue(column.key)
|
|
156
|
+
cellElement.textContent = column.formatter ?
|
|
157
|
+
column.formatter(value, row.original) :
|
|
158
|
+
String(value ?? '')
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const updateRowElement = (rowElement: HTMLTableRowElement, row: Row<DataGridItem>, index: number): void => {
|
|
163
|
+
if (rowElement.dataset.coreuiLayout !== layoutSignature) {
|
|
164
|
+
buildRowCells(rowElement)
|
|
165
|
+
delete rowElement.dataset.coreuiRow
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (context.selection) {
|
|
169
|
+
const input = rowElement.firstElementChild!.firstElementChild as HTMLInputElement
|
|
170
|
+
const selected = row.getIsSelected()
|
|
171
|
+
input.checked = selected
|
|
172
|
+
rowElement.classList.toggle(CLASS_NAME_ROW_SELECTED, selected)
|
|
173
|
+
rowElement.setAttribute('aria-selected', String(selected))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Cell content is immutable for a given (data version, row id, index)
|
|
177
|
+
// triple — data replacements (dataProvider loads, update()) bump the
|
|
178
|
+
// version — so recycled elements skip the cell pass. This keeps custom
|
|
179
|
+
// render callbacks off the scroll hot path, where all three are stable.
|
|
180
|
+
const rowKey = `${context.getDataVersion()}:${row.id}`
|
|
181
|
+
if (rowElement.dataset.coreuiRow !== rowKey || rowElement.dataset.coreuiIndex !== String(index)) {
|
|
182
|
+
updateRowCells(rowElement, row, index)
|
|
183
|
+
rowElement.dataset.coreuiRow = rowKey
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
rowElement.dataset.coreuiIndex = String(index)
|
|
187
|
+
rowElement.setAttribute('aria-rowindex', String(context.headerRowCount + context.getRowIndexOffset() + index + 1))
|
|
188
|
+
rowElement.classList.toggle(CLASS_NAME_ROW_ODD, index % 2 === 1)
|
|
189
|
+
|
|
190
|
+
if (currentPinning) {
|
|
191
|
+
const pinning = currentPinning
|
|
192
|
+
const offset = context.selection ? 1 : 0
|
|
193
|
+
if (context.selection) {
|
|
194
|
+
applyPinnedCell(rowElement.children[0] as HTMLTableCellElement, pinning.select)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
for (const [cellIndex, column] of currentColumns.entries()) {
|
|
198
|
+
applyPinnedCell(rowElement.children[cellIndex + offset] as HTMLTableCellElement, pinning.columns[column.key] ?? null)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const createSpacer = (): HTMLTableRowElement => {
|
|
204
|
+
const rowElement = document.createElement('tr')
|
|
205
|
+
rowElement.classList.add(CLASS_NAME_SPACER)
|
|
206
|
+
rowElement.setAttribute('aria-hidden', 'true')
|
|
207
|
+
|
|
208
|
+
const cellElement = document.createElement('td')
|
|
209
|
+
cellElement.colSpan = Math.max(cellCount, 1)
|
|
210
|
+
rowElement.append(cellElement)
|
|
211
|
+
|
|
212
|
+
return rowElement
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const setSpacerHeight = (spacerElement: HTMLTableRowElement, height: number): void => {
|
|
216
|
+
spacerElement.style.height = `${height}px`;
|
|
217
|
+
(spacerElement.firstElementChild as HTMLTableCellElement).style.height = `${height}px`
|
|
218
|
+
spacerElement.style.display = height > 0 ? '' : 'none'
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const topSpacer = createSpacer()
|
|
222
|
+
const bottomSpacer = createSpacer()
|
|
223
|
+
let spacersAttached = false
|
|
224
|
+
let emptyElement: HTMLTableRowElement | null = null
|
|
225
|
+
let rowElements: HTMLTableRowElement[] = []
|
|
226
|
+
let currentPinning: GridPinning | null = null
|
|
227
|
+
const rowPool: HTMLTableRowElement[] = []
|
|
228
|
+
|
|
229
|
+
const renderEmpty = () => {
|
|
230
|
+
spacersAttached = false
|
|
231
|
+
rowPool.push(...rowElements)
|
|
232
|
+
rowElements = []
|
|
233
|
+
|
|
234
|
+
if (!emptyElement) {
|
|
235
|
+
emptyElement = document.createElement('tr')
|
|
236
|
+
emptyElement.classList.add(CLASS_NAME_EMPTY)
|
|
237
|
+
emptyElement.append(document.createElement('td'))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const cellElement = emptyElement.firstElementChild as HTMLTableCellElement
|
|
241
|
+
cellElement.colSpan = Math.max(cellCount, 1)
|
|
242
|
+
setCellContent(cellElement, context.renderEmpty(), context.sanitizeContent)
|
|
243
|
+
|
|
244
|
+
element.replaceChildren(emptyElement)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const renderAll = (rows: Array<Row<DataGridItem>>) => {
|
|
248
|
+
if (emptyElement) {
|
|
249
|
+
emptyElement.remove()
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
while (rowElements.length > rows.length) {
|
|
253
|
+
const rowElement = rowElements.pop()!
|
|
254
|
+
rowElement.remove()
|
|
255
|
+
rowPool.push(rowElement)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
while (rowElements.length < rows.length) {
|
|
259
|
+
const rowElement = rowPool.pop() ?? createRowElement()
|
|
260
|
+
element.append(rowElement)
|
|
261
|
+
rowElements.push(rowElement)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const [index, row] of rows.entries()) {
|
|
265
|
+
updateRowElement(rowElements[index], row, index)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const renderWindow = (rows: Array<Row<DataGridItem>>, virtualizer: GridVirtualizerInstance) => {
|
|
270
|
+
if (!spacersAttached) {
|
|
271
|
+
element.replaceChildren(topSpacer, bottomSpacer)
|
|
272
|
+
spacersAttached = true
|
|
273
|
+
rowElements = []
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const virtualItems = virtualizer.getVirtualItems()
|
|
277
|
+
const totalSize = virtualizer.getTotalSize()
|
|
278
|
+
const paddingTop = virtualItems.length > 0 ? virtualItems[0].start : 0
|
|
279
|
+
const paddingBottom = virtualItems.length > 0 ?
|
|
280
|
+
totalSize - virtualItems[virtualItems.length - 1].end :
|
|
281
|
+
totalSize
|
|
282
|
+
|
|
283
|
+
setSpacerHeight(topSpacer, paddingTop)
|
|
284
|
+
setSpacerHeight(bottomSpacer, paddingBottom)
|
|
285
|
+
|
|
286
|
+
while (rowElements.length > virtualItems.length) {
|
|
287
|
+
const rowElement = rowElements.pop()!
|
|
288
|
+
rowElement.remove()
|
|
289
|
+
rowPool.push(rowElement)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
while (rowElements.length < virtualItems.length) {
|
|
293
|
+
const rowElement = rowPool.pop() ?? createRowElement()
|
|
294
|
+
bottomSpacer.before(rowElement)
|
|
295
|
+
rowElements.push(rowElement)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const [index, virtualItem] of virtualItems.entries()) {
|
|
299
|
+
updateRowElement(rowElements[index], rows[virtualItem.index], virtualItem.index)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
let editingSignature: string | null = null
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
element,
|
|
307
|
+
update() {
|
|
308
|
+
const rows = context.getRows()
|
|
309
|
+
currentPinning = context.getPinning()
|
|
310
|
+
|
|
311
|
+
// The immutable-cell contract skips the cell pass on recycled rows, so
|
|
312
|
+
// an editing transition (start, commit, cancel, move) must force the
|
|
313
|
+
// rendered rows to rebuild their cells - once, not per frame.
|
|
314
|
+
const editingCell = context.getEditingCell()
|
|
315
|
+
const nextEditingSignature = editingCell ? `${editingCell.rowId}:${editingCell.columnId}` : null
|
|
316
|
+
if (nextEditingSignature !== editingSignature) {
|
|
317
|
+
editingSignature = nextEditingSignature
|
|
318
|
+
for (const rowElement of rowElements) {
|
|
319
|
+
delete rowElement.dataset.coreuiRow
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const columns = context.getVisibleColumns()
|
|
324
|
+
const signature = columns.map(column => column.key).join('|')
|
|
325
|
+
if (signature !== layoutSignature) {
|
|
326
|
+
currentColumns = columns
|
|
327
|
+
layoutSignature = signature
|
|
328
|
+
cellCount = currentColumns.length + (context.selection ? 1 : 0);
|
|
329
|
+
(topSpacer.firstElementChild as HTMLTableCellElement).colSpan = Math.max(cellCount, 1);
|
|
330
|
+
(bottomSpacer.firstElementChild as HTMLTableCellElement).colSpan = Math.max(cellCount, 1)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (rows.length === 0) {
|
|
334
|
+
renderEmpty()
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const virtualizer = context.getVirtualizer()
|
|
339
|
+
|
|
340
|
+
if (virtualizer) {
|
|
341
|
+
renderWindow(rows, virtualizer)
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
renderAll(rows)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|