@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/data-grid.ts
ADDED
|
@@ -0,0 +1,2208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Table, TableState } from '@tanstack/table-core'
|
|
9
|
+
import BaseComponent from './base-component.js'
|
|
10
|
+
import EventHandler from './dom/event-handler.js'
|
|
11
|
+
import {
|
|
12
|
+
createActiveCellManager, resolveNavigation, type ActiveCell, type ActiveCellContext, type ActiveCellManager
|
|
13
|
+
} from './engine/active-cell'
|
|
14
|
+
import { createDataSource, type GridDataSource } from './engine/data-source'
|
|
15
|
+
import { createScheduler, type Scheduler } from './engine/scheduler'
|
|
16
|
+
import { createGridTable } from './engine/table'
|
|
17
|
+
import { createGridVirtualizer, type GridVirtualizer } from './engine/virtualizer'
|
|
18
|
+
import {
|
|
19
|
+
DEFAULT_COLUMN_MENU_ICON, DEFAULT_FILTER_ICON, DEFAULT_HIDE_COLUMN_ICON, DEFAULT_MOVE_LEFT_ICON, DEFAULT_MOVE_RIGHT_ICON,
|
|
20
|
+
DEFAULT_PIN_LEFT_ICON, DEFAULT_PIN_RIGHT_ICON, DEFAULT_SORT_ASCENDING_ICON, DEFAULT_SORT_DESCENDING_ICON,
|
|
21
|
+
DEFAULT_SORT_NEUTRAL_ICON, DEFAULT_TOOLBAR_COLUMNS_ICON, DEFAULT_TOOLBAR_EXPORT_ICON,
|
|
22
|
+
DEFAULT_TOOLBAR_REDO_ICON, DEFAULT_TOOLBAR_UNDO_ICON, DEFAULT_UNPIN_ICON
|
|
23
|
+
} from './icons'
|
|
24
|
+
import { DEFAULT_LABELS, interpolate } from './labels'
|
|
25
|
+
import type {
|
|
26
|
+
DataGridColumn, DataGridColumnFilterValue, DataGridEditableOptions, DataGridEditor, DataGridEditorContext,
|
|
27
|
+
DataGridItem, DataGridLabels, DataGridMenuAction, DataGridOptions, DataGridSlot, DataGridSlotContext, DataGridState
|
|
28
|
+
} from './types'
|
|
29
|
+
import { createBody, type GridBody } from './ui/body'
|
|
30
|
+
import { createBuiltInEditor } from './ui/editor'
|
|
31
|
+
import { createHeader, type GridHeader } from './ui/header'
|
|
32
|
+
import { createLoading, type GridLoading } from './ui/loading'
|
|
33
|
+
import { createFilterMenu, type GridFilterMenu } from './ui/filter-menu'
|
|
34
|
+
import { createMenu, type GridMenu } from './ui/menu'
|
|
35
|
+
import { createPagination } from './ui/pagination'
|
|
36
|
+
import type { GridPinning, PinnedCellInfo } from './ui/pinning'
|
|
37
|
+
import { createColumnChooser, type ColumnChooserColumn, type GridColumnChooser } from './ui/column-chooser'
|
|
38
|
+
import { createToolbar, resolveToolbar, type ResolvedToolbar } from './ui/toolbar'
|
|
39
|
+
import { createTooltip, type GridTooltip } from './ui/tooltip'
|
|
40
|
+
import {
|
|
41
|
+
downloadCsv, exportCsv, type CsvDownloadOptions, type CsvOptions
|
|
42
|
+
} from './util/csv'
|
|
43
|
+
import { debounce } from './util/debounce'
|
|
44
|
+
import { defineJQueryPlugin } from './util/index.js'
|
|
45
|
+
import { DefaultAllowlist, sanitizeHtml, SVGAllowlist } from './util/sanitizer.js'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Constants
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const NAME = 'data-grid'
|
|
52
|
+
const DATA_KEY = 'coreui.data-grid'
|
|
53
|
+
const EVENT_KEY = `.${DATA_KEY}`
|
|
54
|
+
|
|
55
|
+
const EVENT_CHANGE = `change${EVENT_KEY}`
|
|
56
|
+
const EVENT_CLICK = `click${EVENT_KEY}`
|
|
57
|
+
const EVENT_DATA_ERROR = `dataError${EVENT_KEY}`
|
|
58
|
+
const EVENT_DATA_LOAD = `dataLoad${EVENT_KEY}`
|
|
59
|
+
const EVENT_DBLCLICK = `dblclick${EVENT_KEY}`
|
|
60
|
+
const EVENT_EDIT_CANCEL = `editCancel${EVENT_KEY}`
|
|
61
|
+
const EVENT_EDIT_COMMIT = `editCommit${EVENT_KEY}`
|
|
62
|
+
const EVENT_EDIT_START = `editStart${EVENT_KEY}`
|
|
63
|
+
const EVENT_FILTER_CHANGE = `filterChange${EVENT_KEY}`
|
|
64
|
+
const EVENT_INPUT = `input${EVENT_KEY}`
|
|
65
|
+
const EVENT_ORDER_CHANGE = `orderChange${EVENT_KEY}`
|
|
66
|
+
const EVENT_PAGINATION_CHANGE = `paginationChange${EVENT_KEY}`
|
|
67
|
+
const EVENT_PINNING_CHANGE = `pinningChange${EVENT_KEY}`
|
|
68
|
+
const EVENT_SELECTION_CHANGE = `selectionChange${EVENT_KEY}`
|
|
69
|
+
const EVENT_SIZING_CHANGE = `sizingChange${EVENT_KEY}`
|
|
70
|
+
const EVENT_SORTING_CHANGE = `sortingChange${EVENT_KEY}`
|
|
71
|
+
const EVENT_VISIBILITY_CHANGE = `visibilityChange${EVENT_KEY}`
|
|
72
|
+
|
|
73
|
+
const EVENT_FOCUSIN = `focusin${EVENT_KEY}`
|
|
74
|
+
const EVENT_FOCUSOUT = `focusout${EVENT_KEY}`
|
|
75
|
+
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
|
|
76
|
+
|
|
77
|
+
const EVENT_DRAGEND = `dragend${EVENT_KEY}`
|
|
78
|
+
const EVENT_DRAGLEAVE = `dragleave${EVENT_KEY}`
|
|
79
|
+
const EVENT_DRAGOVER = `dragover${EVENT_KEY}`
|
|
80
|
+
const EVENT_DRAGSTART = `dragstart${EVENT_KEY}`
|
|
81
|
+
const EVENT_DROP = `drop${EVENT_KEY}`
|
|
82
|
+
const EVENT_MOUSEDOWN = `mousedown${EVENT_KEY}`
|
|
83
|
+
const EVENT_POINTERDOWN = `pointerdown${EVENT_KEY}`
|
|
84
|
+
const EVENT_TOUCHSTART = `touchstart${EVENT_KEY}`
|
|
85
|
+
|
|
86
|
+
const CLASS_NAME_DATA_GRID = 'data-grid'
|
|
87
|
+
const CLASS_NAME_DRAGGING = 'data-grid-dragging'
|
|
88
|
+
const CLASS_NAME_EDITOR_POPUP = 'data-grid-editor-popup'
|
|
89
|
+
const CLASS_NAME_DROP_TARGET = 'data-grid-drop-target'
|
|
90
|
+
const CLASS_NAME_PAGINATION_TOP = 'data-grid-pagination-top'
|
|
91
|
+
const CLASS_NAME_PINNED = 'data-grid-pinned'
|
|
92
|
+
const CLASS_NAME_RESIZABLE = 'data-grid-resizable'
|
|
93
|
+
const CLASS_NAME_TABLE = 'table'
|
|
94
|
+
const CLASS_NAME_VIEWPORT = 'data-grid-viewport'
|
|
95
|
+
|
|
96
|
+
const SELECTOR_COLUMN_FILTER = 'input[data-coreui-column-filter]'
|
|
97
|
+
const SELECTOR_DRAGGABLE = 'th[data-coreui-draggable]'
|
|
98
|
+
const SELECTOR_MENU_BUTTON = 'button[data-coreui-menu]'
|
|
99
|
+
const SELECTOR_FILTER_BUTTON = 'button[data-coreui-filter]'
|
|
100
|
+
const SELECTOR_GLOBAL_FILTER = 'input[data-coreui-global-filter]'
|
|
101
|
+
const SELECTOR_PAGE = 'button[data-coreui-page]'
|
|
102
|
+
const SELECTOR_PAGE_SIZE = 'select[data-coreui-page-size]'
|
|
103
|
+
const SELECTOR_RESIZER = '[data-coreui-resizer]'
|
|
104
|
+
const SELECTOR_ROW_SELECT = 'input[data-coreui-row-select]'
|
|
105
|
+
const SELECTOR_SELECT_ALL = 'input[data-coreui-select-all]'
|
|
106
|
+
const SELECTOR_SORT_BUTTON = '.data-grid-sort-button'
|
|
107
|
+
const SELECTOR_TOOLBAR_COLUMNS = 'button[data-coreui-toolbar-columns]'
|
|
108
|
+
const SELECTOR_TOOLBAR_EXPORT = 'button[data-coreui-toolbar-export]'
|
|
109
|
+
const SELECTOR_TOOLBAR_REDO = 'button[data-coreui-toolbar-redo]'
|
|
110
|
+
const SELECTOR_TOOLBAR_UNDO = 'button[data-coreui-toolbar-undo]'
|
|
111
|
+
|
|
112
|
+
const FILTER_DEBOUNCE_MS = 250
|
|
113
|
+
const DEFAULT_PAGE_SIZE_OPTIONS = [5, 10, 20, 50]
|
|
114
|
+
|
|
115
|
+
let editorErrorCount = 0
|
|
116
|
+
|
|
117
|
+
// History entries hold the ROW ID, not the item reference - an immutable
|
|
118
|
+
// app replaces the object on every commit, so undo/redo resolve the current
|
|
119
|
+
// item from the core row model at emit time.
|
|
120
|
+
interface DataGridEditCommitEntry {
|
|
121
|
+
rowId: string
|
|
122
|
+
columnId: string
|
|
123
|
+
value: unknown
|
|
124
|
+
previousValue: unknown
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const STATE_STORAGE_PREFIX = 'coreui-data-grid:'
|
|
128
|
+
const STATE_AUTOSAVE_DEBOUNCE_MS = 250
|
|
129
|
+
const HISTORY_LIMIT = 100
|
|
130
|
+
|
|
131
|
+
const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]'
|
|
132
|
+
|
|
133
|
+
const applyTabIndex = (cellElement: HTMLElement, isTabbable: boolean) => {
|
|
134
|
+
const value = isTabbable ? 0 : -1
|
|
135
|
+
if (cellElement.tabIndex !== value) {
|
|
136
|
+
cellElement.tabIndex = value
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const Default: DataGridOptions = {
|
|
141
|
+
cellNavigation: false,
|
|
142
|
+
columnFilters: false,
|
|
143
|
+
columnMenu: false,
|
|
144
|
+
columnMenuIcon: DEFAULT_COLUMN_MENU_ICON,
|
|
145
|
+
columnOrder: false,
|
|
146
|
+
columnPinning: false,
|
|
147
|
+
columnSizing: false,
|
|
148
|
+
columnVisibility: false,
|
|
149
|
+
columns: [],
|
|
150
|
+
dataProvider: null,
|
|
151
|
+
editing: false,
|
|
152
|
+
empty: 'No items found',
|
|
153
|
+
filterIcon: DEFAULT_FILTER_ICON,
|
|
154
|
+
globalFilter: false,
|
|
155
|
+
hideColumnIcon: DEFAULT_HIDE_COLUMN_ICON,
|
|
156
|
+
history: false,
|
|
157
|
+
itemKey: null,
|
|
158
|
+
items: [],
|
|
159
|
+
labels: {},
|
|
160
|
+
moveLeftIcon: DEFAULT_MOVE_LEFT_ICON,
|
|
161
|
+
moveRightIcon: DEFAULT_MOVE_RIGHT_ICON,
|
|
162
|
+
overscan: 10,
|
|
163
|
+
pagination: false,
|
|
164
|
+
pinLeftIcon: DEFAULT_PIN_LEFT_ICON,
|
|
165
|
+
pinRightIcon: DEFAULT_PIN_RIGHT_ICON,
|
|
166
|
+
rowHeight: 44,
|
|
167
|
+
rowSelection: false,
|
|
168
|
+
sanitize: true,
|
|
169
|
+
sanitizeFn: null,
|
|
170
|
+
slots: {},
|
|
171
|
+
sortAscendingIcon: DEFAULT_SORT_ASCENDING_ICON,
|
|
172
|
+
sortDescendingIcon: DEFAULT_SORT_DESCENDING_ICON,
|
|
173
|
+
sortNeutralIcon: DEFAULT_SORT_NEUTRAL_ICON,
|
|
174
|
+
sorterVisibility: false,
|
|
175
|
+
sorting: true,
|
|
176
|
+
stateKey: null,
|
|
177
|
+
toolbar: false,
|
|
178
|
+
toolbarColumnsIcon: DEFAULT_TOOLBAR_COLUMNS_ICON,
|
|
179
|
+
toolbarExportIcon: DEFAULT_TOOLBAR_EXPORT_ICON,
|
|
180
|
+
toolbarRedoIcon: DEFAULT_TOOLBAR_REDO_ICON,
|
|
181
|
+
toolbarUndoIcon: DEFAULT_TOOLBAR_UNDO_ICON,
|
|
182
|
+
unpinIcon: DEFAULT_UNPIN_ICON,
|
|
183
|
+
virtualization: true
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const DefaultType = {
|
|
187
|
+
cellNavigation: 'boolean',
|
|
188
|
+
columnFilters: 'boolean',
|
|
189
|
+
columnMenu: '(boolean|function)',
|
|
190
|
+
columnMenuIcon: 'string',
|
|
191
|
+
columnOrder: '(boolean|array)',
|
|
192
|
+
columnPinning: '(boolean|object)',
|
|
193
|
+
columnSizing: '(boolean|object)',
|
|
194
|
+
columnVisibility: '(boolean|object)',
|
|
195
|
+
columns: 'array',
|
|
196
|
+
dataProvider: '(function|null)',
|
|
197
|
+
editing: 'boolean',
|
|
198
|
+
empty: '(string|function)',
|
|
199
|
+
filterIcon: 'string',
|
|
200
|
+
globalFilter: 'boolean',
|
|
201
|
+
hideColumnIcon: 'string',
|
|
202
|
+
history: 'boolean',
|
|
203
|
+
itemKey: '(function|null)',
|
|
204
|
+
items: 'array',
|
|
205
|
+
labels: 'object',
|
|
206
|
+
moveLeftIcon: 'string',
|
|
207
|
+
moveRightIcon: 'string',
|
|
208
|
+
overscan: 'number',
|
|
209
|
+
pagination: '(boolean|object)',
|
|
210
|
+
pinLeftIcon: 'string',
|
|
211
|
+
pinRightIcon: 'string',
|
|
212
|
+
rowHeight: 'number',
|
|
213
|
+
rowSelection: '(boolean|object)',
|
|
214
|
+
sanitize: 'boolean',
|
|
215
|
+
sanitizeFn: '(function|null)',
|
|
216
|
+
slots: 'object',
|
|
217
|
+
sortAscendingIcon: 'string',
|
|
218
|
+
sortDescendingIcon: 'string',
|
|
219
|
+
sortNeutralIcon: 'string',
|
|
220
|
+
sorterVisibility: '(string|boolean)',
|
|
221
|
+
sorting: '(boolean|object)',
|
|
222
|
+
stateKey: '(string|null)',
|
|
223
|
+
toolbar: '(boolean|object)',
|
|
224
|
+
toolbarColumnsIcon: 'string',
|
|
225
|
+
toolbarExportIcon: 'string',
|
|
226
|
+
toolbarRedoIcon: 'string',
|
|
227
|
+
toolbarUndoIcon: 'string',
|
|
228
|
+
unpinIcon: 'string',
|
|
229
|
+
virtualization: 'boolean'
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Class definition
|
|
234
|
+
*/
|
|
235
|
+
|
|
236
|
+
class DataGrid extends BaseComponent {
|
|
237
|
+
declare _config: DataGridOptions
|
|
238
|
+
private _table!: Table<DataGridItem>
|
|
239
|
+
private _scheduler!: Scheduler
|
|
240
|
+
private _virtualizer: GridVirtualizer | null = null
|
|
241
|
+
private _header!: GridHeader
|
|
242
|
+
private _body!: GridBody
|
|
243
|
+
private _toolbar: DataGridSlot | null = null
|
|
244
|
+
private _paginations: Array<{ element: HTMLElement; refresh: () => void; dispose?: () => void }> = []
|
|
245
|
+
private _emptySlot: DataGridSlot | null = null
|
|
246
|
+
private _dataSource: GridDataSource | null = null
|
|
247
|
+
private _loading: GridLoading | null = null
|
|
248
|
+
private _loadFailed = false
|
|
249
|
+
private _dataVersion = 0
|
|
250
|
+
private _viewportElement!: HTMLDivElement
|
|
251
|
+
private _tableElement!: HTMLTableElement
|
|
252
|
+
private _liveRegion: HTMLElement | null = null
|
|
253
|
+
private _labels!: DataGridLabels
|
|
254
|
+
private _selectionAnchorIndex: number | null = null
|
|
255
|
+
private _activeCell: ActiveCellManager | null = null
|
|
256
|
+
private _containsFocus = false
|
|
257
|
+
private _editingCell: {
|
|
258
|
+
rowId: string
|
|
259
|
+
columnId: string
|
|
260
|
+
rowIndex: number
|
|
261
|
+
item: DataGridItem
|
|
262
|
+
previousValue: unknown
|
|
263
|
+
} | null = null
|
|
264
|
+
private _undoStack: DataGridEditCommitEntry[] = []
|
|
265
|
+
private _redoStack: DataGridEditCommitEntry[] = []
|
|
266
|
+
private _persistState: (() => void) | null = null
|
|
267
|
+
private _editor: DataGridEditor | null = null
|
|
268
|
+
// Removed by reference - a namespace-wide off(document, ...) would tear
|
|
269
|
+
// down every other grid instance's outside-commit handler with it.
|
|
270
|
+
private _editorPointerdownHandler: ((event: Event) => void) | null = null
|
|
271
|
+
private _editorPopup: HTMLElement | null = null
|
|
272
|
+
private _editorErrorElement: HTMLElement | null = null
|
|
273
|
+
private _editorFocusPending = false
|
|
274
|
+
private _columnsByKey = new Map<string, DataGridColumn>()
|
|
275
|
+
private _dragColumn: string | null = null
|
|
276
|
+
private _menu: GridMenu | null = null
|
|
277
|
+
private _tooltip: GridTooltip | null = null
|
|
278
|
+
private _filterMenu: GridFilterMenu | null = null
|
|
279
|
+
private _columnChooser: GridColumnChooser | null = null
|
|
280
|
+
private _toolbarConfig: ResolvedToolbar | null = null
|
|
281
|
+
|
|
282
|
+
constructor(element: Element | string, config?: Partial<DataGridOptions>) {
|
|
283
|
+
super(element, config)
|
|
284
|
+
|
|
285
|
+
if (!this._element) {
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
this._labels = { ...DEFAULT_LABELS, ...this._config.labels }
|
|
290
|
+
this._scheduler = createScheduler(() => this._update())
|
|
291
|
+
this._table = this._createTable()
|
|
292
|
+
|
|
293
|
+
this._liveRegion = document.createElement('div')
|
|
294
|
+
this._liveRegion.classList.add('visually-hidden')
|
|
295
|
+
this._liveRegion.setAttribute('aria-live', 'polite')
|
|
296
|
+
this._element.append(this._liveRegion)
|
|
297
|
+
|
|
298
|
+
this._render()
|
|
299
|
+
this._addEventListeners()
|
|
300
|
+
this._tooltip = createTooltip(this._element)
|
|
301
|
+
// A persisted view restores before the first server fetch, so the initial
|
|
302
|
+
// request already carries the user's sorting/filters/page.
|
|
303
|
+
this._restorePersistedState()
|
|
304
|
+
this._initDataSource()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Getters
|
|
308
|
+
get table(): Table<DataGridItem> {
|
|
309
|
+
return this._table
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
static get Default(): DataGridOptions {
|
|
313
|
+
return Default
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
static get DefaultType(): Record<string, string> {
|
|
317
|
+
return DefaultType
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
static get NAME(): string {
|
|
321
|
+
return NAME
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
static getInstance(element: Element | string | null): DataGrid | null {
|
|
325
|
+
return super.getInstance(element) as DataGrid | null
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
static getOrCreateInstance(element: Element | string, config?: Partial<DataGridOptions>): DataGrid {
|
|
329
|
+
return super.getOrCreateInstance(element, config) as DataGrid
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Public
|
|
333
|
+
getSelectedItems(): DataGridItem[] {
|
|
334
|
+
return this._table.getSelectedRowModel().rows.map(row => row.original)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
getCsv(options?: CsvOptions): string {
|
|
338
|
+
return exportCsv(this._table, options)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
downloadCsv(options?: CsvDownloadOptions): void {
|
|
342
|
+
downloadCsv(this._table, options)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// A serializable snapshot of every user-adjustable state slice. Feed it
|
|
346
|
+
// back through restoreState() - scroll position and edit drafts excluded.
|
|
347
|
+
getState(): DataGridState {
|
|
348
|
+
const state = this._table.getState()
|
|
349
|
+
return {
|
|
350
|
+
columnFilters: state.columnFilters,
|
|
351
|
+
columnOrder: state.columnOrder,
|
|
352
|
+
columnPinning: state.columnPinning,
|
|
353
|
+
columnSizing: state.columnSizing,
|
|
354
|
+
columnVisibility: state.columnVisibility,
|
|
355
|
+
globalFilter: (state.globalFilter as string | undefined) ?? '',
|
|
356
|
+
pagination: state.pagination,
|
|
357
|
+
rowSelection: state.rowSelection,
|
|
358
|
+
sorting: state.sorting
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
restoreState(state: Partial<DataGridState>): void {
|
|
363
|
+
this._table.setState(previous => ({ ...previous, ...state }))
|
|
364
|
+
|
|
365
|
+
// The built-in search input is uncontrolled - sync it to the restored
|
|
366
|
+
// filter so the UI matches the state.
|
|
367
|
+
if (state.globalFilter !== undefined) {
|
|
368
|
+
const input = this._element.querySelector<HTMLInputElement>(SELECTOR_GLOBAL_FILTER)
|
|
369
|
+
if (input) {
|
|
370
|
+
input.value = state.globalFilter
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
this._scheduler.schedule()
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Re-emits the last edit commit inverted (value and previousValue swap) -
|
|
378
|
+
// the app reverts the data through the same editCommit channel it already
|
|
379
|
+
// handles. New commits clear the redo stack.
|
|
380
|
+
undo(): void {
|
|
381
|
+
this._applyHistoryEntry(this._undoStack, this._redoStack, true)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
redo(): void {
|
|
385
|
+
this._applyHistoryEntry(this._redoStack, this._undoStack, false)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
private _applyHistoryEntry(
|
|
389
|
+
from: DataGridEditCommitEntry[],
|
|
390
|
+
to: DataGridEditCommitEntry[],
|
|
391
|
+
inverted: boolean
|
|
392
|
+
): void {
|
|
393
|
+
const entry = from.pop()
|
|
394
|
+
if (!entry) {
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// The row may be gone (data replaced without it) - drop the entry.
|
|
399
|
+
const row = this._table.getCoreRowModel().rowsById[entry.rowId]
|
|
400
|
+
if (!row) {
|
|
401
|
+
this._updateHistoryButtons()
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
to.push(entry)
|
|
406
|
+
EventHandler.trigger(this._element, EVENT_EDIT_COMMIT, {
|
|
407
|
+
item: row.original,
|
|
408
|
+
columnId: entry.columnId,
|
|
409
|
+
value: inverted ? entry.previousValue : entry.value,
|
|
410
|
+
previousValue: inverted ? entry.value : entry.previousValue
|
|
411
|
+
})
|
|
412
|
+
this._announce(inverted ? this._labels.undoneAnnouncement : this._labels.redoneAnnouncement)
|
|
413
|
+
this._updateHistoryButtons()
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Replaces the data in place, preserving sorting/filters/selection/scroll.
|
|
417
|
+
// No-op in server-side mode - the dataProvider owns the data there.
|
|
418
|
+
setItems(items: DataGridItem[]): void {
|
|
419
|
+
if (this._config.dataProvider) {
|
|
420
|
+
return
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// External data replacement invalidates the draft - the app owns the data.
|
|
424
|
+
this._stopEditing({ commit: false })
|
|
425
|
+
this._config.items = items
|
|
426
|
+
this._dataVersion++
|
|
427
|
+
this._table.setOptions(previous => ({ ...previous, data: items }))
|
|
428
|
+
this._scheduler.schedule()
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Full rebuild: recreates the table, wiping sorting/filters/selection/scroll.
|
|
432
|
+
// Use setItems() to swap data while keeping user state.
|
|
433
|
+
update(config: Partial<DataGridOptions>): void {
|
|
434
|
+
this._config = this._getConfig({ ...this._config, ...config })
|
|
435
|
+
this._labels = { ...DEFAULT_LABELS, ...this._config.labels }
|
|
436
|
+
|
|
437
|
+
this._teardownUi()
|
|
438
|
+
this._selectionAnchorIndex = null
|
|
439
|
+
this._loadFailed = false
|
|
440
|
+
this._dataVersion++
|
|
441
|
+
this._table = this._createTable()
|
|
442
|
+
this._render()
|
|
443
|
+
this._initDataSource()
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
dispose(): void {
|
|
447
|
+
this._teardownUi()
|
|
448
|
+
this._scheduler.dispose()
|
|
449
|
+
this._tooltip?.dispose()
|
|
450
|
+
this._tooltip = null
|
|
451
|
+
if (this._liveRegion) {
|
|
452
|
+
this._liveRegion.remove()
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
this._element.classList.remove(CLASS_NAME_DATA_GRID)
|
|
456
|
+
this._element.removeAttribute('data-coreui-sorter-visibility')
|
|
457
|
+
|
|
458
|
+
super.dispose()
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Private
|
|
462
|
+
// Editing needs the active-cell substrate (Enter/F2 target the active cell).
|
|
463
|
+
_configAfterMerge(config: DataGridOptions): DataGridOptions {
|
|
464
|
+
if (config.editing) {
|
|
465
|
+
config.cellNavigation = true
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return config
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private _createTable(): Table<DataGridItem> {
|
|
472
|
+
return createGridTable(this._config, (previous, next) => {
|
|
473
|
+
this._notifyStateChange(previous, next)
|
|
474
|
+
this._scheduler.schedule()
|
|
475
|
+
})
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private _teardownUi(): void {
|
|
479
|
+
this._stopEditing({ commit: false })
|
|
480
|
+
|
|
481
|
+
if (this._dataSource) {
|
|
482
|
+
this._dataSource.dispose()
|
|
483
|
+
this._dataSource = null
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
this._header?.dispose()
|
|
487
|
+
|
|
488
|
+
if (this._menu) {
|
|
489
|
+
this._menu.dispose()
|
|
490
|
+
this._menu = null
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (this._filterMenu) {
|
|
494
|
+
this._filterMenu.dispose()
|
|
495
|
+
this._filterMenu = null
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (this._columnChooser) {
|
|
499
|
+
this._columnChooser.dispose()
|
|
500
|
+
this._columnChooser = null
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (this._loading) {
|
|
504
|
+
this._loading.element.remove()
|
|
505
|
+
this._loading = null
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (this._virtualizer) {
|
|
509
|
+
this._virtualizer.destroy()
|
|
510
|
+
this._virtualizer = null
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (this._toolbar) {
|
|
514
|
+
this._toolbar.dispose?.()
|
|
515
|
+
this._toolbar.element.remove()
|
|
516
|
+
this._toolbar = null
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
for (const pagination of this._paginations) {
|
|
520
|
+
pagination.dispose?.()
|
|
521
|
+
pagination.element.remove()
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
this._paginations = []
|
|
525
|
+
|
|
526
|
+
if (this._emptySlot) {
|
|
527
|
+
this._emptySlot.dispose?.()
|
|
528
|
+
this._emptySlot = null
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (this._viewportElement) {
|
|
532
|
+
this._viewportElement.remove()
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
private _slotContext(): DataGridSlotContext {
|
|
537
|
+
return { table: this._table, labels: this._labels }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
private _isColumnSortable(column: DataGridColumn): boolean {
|
|
541
|
+
return Boolean(this._config.sorting) && column.sortable !== false
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private _isColumnFilterable(column: DataGridColumn): boolean {
|
|
545
|
+
return this._config.columnFilters && column.filterable !== false
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private _isColumnResizable(column: DataGridColumn): boolean {
|
|
549
|
+
return Boolean(this._config.columnSizing) && column.resizable !== false
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private _sanitizeIcon(icon: string): string {
|
|
553
|
+
return this._config.sanitize ? sanitizeHtml(icon, SVGAllowlist, this._config.sanitizeFn) : icon
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
private _hasColumnMenu(column: DataGridColumn): boolean {
|
|
557
|
+
if (!this._config.columnMenu) {
|
|
558
|
+
return false
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// A builder can add items regardless of enabled features, so ask it directly.
|
|
562
|
+
if (typeof this._config.columnMenu === 'function') {
|
|
563
|
+
return this._getMenuActions(column.key).length > 0
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
this._isColumnSortable(column) ||
|
|
568
|
+
Boolean(this._config.columnPinning) ||
|
|
569
|
+
(Boolean(this._config.columnVisibility) && column.hideable !== false) ||
|
|
570
|
+
(Boolean(this._config.columnOrder) && column.movable !== false)
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
private _renderToolbar(): void {
|
|
575
|
+
const toolbarConfig = resolveToolbar(this._config)
|
|
576
|
+
this._toolbarConfig = toolbarConfig
|
|
577
|
+
|
|
578
|
+
if (this._config.slots.toolbar) {
|
|
579
|
+
this._toolbar = this._config.slots.toolbar(this._slotContext())
|
|
580
|
+
this._element.append(this._toolbar.element)
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
if (!toolbarConfig.columns && !toolbarConfig.export && !toolbarConfig.search && !toolbarConfig.history) {
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
this._toolbar = createToolbar({
|
|
589
|
+
columns: toolbarConfig.columns,
|
|
590
|
+
export: toolbarConfig.export,
|
|
591
|
+
icons: {
|
|
592
|
+
columns: this._sanitizeIcon(this._config.toolbarColumnsIcon),
|
|
593
|
+
export: this._sanitizeIcon(this._config.toolbarExportIcon),
|
|
594
|
+
redo: this._sanitizeIcon(this._config.toolbarRedoIcon),
|
|
595
|
+
undo: this._sanitizeIcon(this._config.toolbarUndoIcon)
|
|
596
|
+
},
|
|
597
|
+
history: toolbarConfig.history,
|
|
598
|
+
labels: this._labels,
|
|
599
|
+
search: toolbarConfig.search
|
|
600
|
+
})
|
|
601
|
+
this._element.append(this._toolbar.element)
|
|
602
|
+
|
|
603
|
+
if (toolbarConfig.columns) {
|
|
604
|
+
this._columnChooser = createColumnChooser({
|
|
605
|
+
root: this._element as HTMLElement,
|
|
606
|
+
labels: this._labels,
|
|
607
|
+
getColumns: () => this._chooserColumns(),
|
|
608
|
+
toggleVisibility: (columnId, visible) => {
|
|
609
|
+
this._table.getColumn(columnId)?.toggleVisibility(visible)
|
|
610
|
+
},
|
|
611
|
+
showAll: () => this._table.toggleAllColumnsVisible(true),
|
|
612
|
+
reset: () => this._table.setColumnVisibility(this._table.initialState.columnVisibility ?? {})
|
|
613
|
+
})
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private _render(): void {
|
|
618
|
+
this._columnsByKey = new Map(this._config.columns.map(column => [column.key, column]))
|
|
619
|
+
this._activeCell = this._config.cellNavigation ?
|
|
620
|
+
createActiveCellManager(() => this._scheduler.schedule()) :
|
|
621
|
+
null
|
|
622
|
+
this._element.classList.add(CLASS_NAME_DATA_GRID)
|
|
623
|
+
this._element.classList.toggle(CLASS_NAME_RESIZABLE, Boolean(this._config.columnSizing))
|
|
624
|
+
this._element.classList.toggle(CLASS_NAME_PINNED, Boolean(this._config.columnPinning))
|
|
625
|
+
if (this._config.sorterVisibility) {
|
|
626
|
+
this._element.setAttribute('data-coreui-sorter-visibility', this._config.sorterVisibility)
|
|
627
|
+
} else {
|
|
628
|
+
this._element.removeAttribute('data-coreui-sorter-visibility')
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const { slots } = this._config
|
|
632
|
+
this._renderToolbar()
|
|
633
|
+
|
|
634
|
+
if (slots.empty) {
|
|
635
|
+
this._emptySlot = slots.empty(this._slotContext())
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const selectionEnabled = Boolean(this._config.rowSelection)
|
|
639
|
+
const selectionOptions = typeof this._config.rowSelection === 'object' ? this._config.rowSelection : {}
|
|
640
|
+
// Server-side mode implies pagination — virtualized infinite scroll over a
|
|
641
|
+
// dataProvider is out of scope until 0.3.0.
|
|
642
|
+
const paginationEnabled = Boolean(this._config.pagination) || Boolean(this._config.dataProvider)
|
|
643
|
+
const paginationOptions = typeof this._config.pagination === 'object' ? this._config.pagination : {}
|
|
644
|
+
|
|
645
|
+
if (this._config.columnMenu) {
|
|
646
|
+
this._menu = createMenu({
|
|
647
|
+
root: this._element as HTMLElement,
|
|
648
|
+
getActions: columnKey => this._getMenuActions(columnKey)
|
|
649
|
+
})
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (this._config.columnFilters) {
|
|
653
|
+
this._filterMenu = createFilterMenu({
|
|
654
|
+
root: this._element as HTMLElement,
|
|
655
|
+
labels: this._labels,
|
|
656
|
+
getColumn: columnKey => this._columnsByKey.get(columnKey) ?? null,
|
|
657
|
+
getFacetedValues: columnKey =>
|
|
658
|
+
[...(this._table.getColumn(columnKey)?.getFacetedUniqueValues().keys() ?? [])],
|
|
659
|
+
getFilterValue: columnKey =>
|
|
660
|
+
this._table.getColumn(columnKey)?.getFilterValue() as DataGridColumnFilterValue | undefined,
|
|
661
|
+
applyFilter: (columnKey, value) => {
|
|
662
|
+
this._table.getColumn(columnKey)?.setFilterValue(value)
|
|
663
|
+
}
|
|
664
|
+
})
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
this._header = createHeader({
|
|
668
|
+
cellNavigation: this._config.cellNavigation,
|
|
669
|
+
columns: this._config.columns,
|
|
670
|
+
draggable: Boolean(this._config.columnOrder),
|
|
671
|
+
filterRow:
|
|
672
|
+
this._config.columnFilters &&
|
|
673
|
+
this._config.columns.some(column => column.filter && this._isColumnFilterable(column)),
|
|
674
|
+
hasMenu: column => this._hasColumnMenu(column),
|
|
675
|
+
hasFilterMenu: column =>
|
|
676
|
+
Boolean(this._filterMenu) && this._isColumnFilterable(column) && !column.filter,
|
|
677
|
+
icons: {
|
|
678
|
+
columnMenu: this._sanitizeIcon(this._config.columnMenuIcon),
|
|
679
|
+
filter: this._sanitizeIcon(this._config.filterIcon),
|
|
680
|
+
sortAscending: this._sanitizeIcon(this._config.sortAscendingIcon),
|
|
681
|
+
sortDescending: this._sanitizeIcon(this._config.sortDescendingIcon),
|
|
682
|
+
sortNeutral: this._sanitizeIcon(this._config.sortNeutralIcon)
|
|
683
|
+
},
|
|
684
|
+
isFilterable: column => this._isColumnFilterable(column),
|
|
685
|
+
isResizable: column => this._isColumnResizable(column),
|
|
686
|
+
isSortable: column => this._isColumnSortable(column),
|
|
687
|
+
labels: this._labels,
|
|
688
|
+
resizable: Boolean(this._config.columnSizing),
|
|
689
|
+
selection: selectionEnabled,
|
|
690
|
+
selectAll: selectionEnabled && selectionOptions.selectAll !== false,
|
|
691
|
+
table: this._table
|
|
692
|
+
})
|
|
693
|
+
this._body = createBody({
|
|
694
|
+
cellNavigation: this._config.cellNavigation,
|
|
695
|
+
getDataVersion: () => this._dataVersion,
|
|
696
|
+
getEditingCell: () =>
|
|
697
|
+
(this._editingCell ? { rowId: this._editingCell.rowId, columnId: this._editingCell.columnId } : null),
|
|
698
|
+
// Popup editors overlay the cell; its content stays in place.
|
|
699
|
+
getEditorElement: () => (this._editor && !this._editor.popup ? this._editor.element : null),
|
|
700
|
+
getPinning: () => (this._config.columnPinning ? this._getPinning() : null),
|
|
701
|
+
getRowIndexOffset: () => {
|
|
702
|
+
if (!this._config.pagination && !this._config.dataProvider) {
|
|
703
|
+
return 0
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const { pageIndex, pageSize } = this._table.getState().pagination
|
|
707
|
+
return pageIndex * pageSize
|
|
708
|
+
},
|
|
709
|
+
getRows: () => this._table.getRowModel().rows,
|
|
710
|
+
getVisibleColumns: () => this._visibleColumns(),
|
|
711
|
+
getVirtualizer: () => this._virtualizer?.instance ?? null,
|
|
712
|
+
headerRowCount: this._header.rowCount,
|
|
713
|
+
labels: this._labels,
|
|
714
|
+
renderEmpty: () => {
|
|
715
|
+
if (this._emptySlot) {
|
|
716
|
+
return this._emptySlot.element
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (this._loadFailed) {
|
|
720
|
+
return this._labels.loadError
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
return typeof this._config.empty === 'function' ? this._config.empty() : this._config.empty
|
|
724
|
+
},
|
|
725
|
+
sanitizeContent: html =>
|
|
726
|
+
(this._config.sanitize ? sanitizeHtml(html, DefaultAllowlist, this._config.sanitizeFn) : html),
|
|
727
|
+
selection: selectionEnabled
|
|
728
|
+
})
|
|
729
|
+
|
|
730
|
+
this._tableElement = document.createElement('table')
|
|
731
|
+
this._tableElement.classList.add(CLASS_NAME_TABLE)
|
|
732
|
+
if (this._config.cellNavigation) {
|
|
733
|
+
this._tableElement.setAttribute('role', 'grid')
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
this._tableElement.append(this._header.element, this._body.element)
|
|
737
|
+
|
|
738
|
+
this._viewportElement = document.createElement('div')
|
|
739
|
+
this._viewportElement.classList.add(CLASS_NAME_VIEWPORT)
|
|
740
|
+
if (this._config.cellNavigation) {
|
|
741
|
+
// Focus parks here when the focused row is recycled out of the window;
|
|
742
|
+
// the keydown fallback then scrolls the active cell back and refocuses.
|
|
743
|
+
this._viewportElement.tabIndex = -1
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
this._viewportElement.append(this._tableElement)
|
|
747
|
+
|
|
748
|
+
if (this._config.dataProvider) {
|
|
749
|
+
this._loading = createLoading(this._labels)
|
|
750
|
+
this._viewportElement.append(this._loading.element)
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
this._element.append(this._viewportElement)
|
|
754
|
+
|
|
755
|
+
if (paginationEnabled) {
|
|
756
|
+
const position = paginationOptions.position ?? 'bottom'
|
|
757
|
+
const paginationSlot = slots.pagination
|
|
758
|
+
const createInstance = () => {
|
|
759
|
+
if (paginationSlot) {
|
|
760
|
+
const slot = paginationSlot(this._slotContext())
|
|
761
|
+
return { element: slot.element, refresh: () => slot.update?.(), dispose: () => slot.dispose?.() }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const instance = createPagination({
|
|
765
|
+
info: paginationOptions.info ?? true,
|
|
766
|
+
labels: this._labels,
|
|
767
|
+
pageSizeOptions: paginationOptions.pageSizeOptions ?? DEFAULT_PAGE_SIZE_OPTIONS
|
|
768
|
+
})
|
|
769
|
+
return { element: instance.element, refresh: () => instance.update(this._paginationState()), dispose: undefined }
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (position === 'top' || position === 'both') {
|
|
773
|
+
const pagination = createInstance()
|
|
774
|
+
// The top-margin class styles the built-in module; a custom slot owns
|
|
775
|
+
// its own markup, so don't impose it (keeps vanilla/React in parity).
|
|
776
|
+
if (!paginationSlot) {
|
|
777
|
+
pagination.element.classList.add(CLASS_NAME_PAGINATION_TOP)
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
this._viewportElement.before(pagination.element)
|
|
781
|
+
this._paginations.push(pagination)
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
if (position === 'bottom' || position === 'both') {
|
|
785
|
+
const pagination = createInstance()
|
|
786
|
+
this._element.append(pagination.element)
|
|
787
|
+
this._paginations.push(pagination)
|
|
788
|
+
}
|
|
789
|
+
} else if (this._config.virtualization) {
|
|
790
|
+
this._virtualizer = createGridVirtualizer({
|
|
791
|
+
count: this._table.getRowModel().rows.length,
|
|
792
|
+
getScrollElement: () => this._viewportElement,
|
|
793
|
+
onChange: () => this._scheduler.schedule(),
|
|
794
|
+
overscan: this._config.overscan,
|
|
795
|
+
rowHeight: this._config.rowHeight
|
|
796
|
+
})
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
this._update()
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
private _addEventListeners(): void {
|
|
803
|
+
// Registered unconditionally (update() can flip cellNavigation on later);
|
|
804
|
+
// the handlers bail out when the feature is off.
|
|
805
|
+
EventHandler.on(this._element, EVENT_KEYDOWN, event => {
|
|
806
|
+
this._onCellNavigationKeydown(event as unknown as KeyboardEvent)
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
EventHandler.on(this._element, EVENT_FOCUSIN, event => {
|
|
810
|
+
this._containsFocus = true
|
|
811
|
+
this._onCellFocusIn(event as unknown as FocusEvent)
|
|
812
|
+
})
|
|
813
|
+
|
|
814
|
+
EventHandler.on(this._element, EVENT_FOCUSOUT, event => {
|
|
815
|
+
const related = (event as unknown as FocusEvent).relatedTarget as Node | null
|
|
816
|
+
if (!related || !this._element.contains(related)) {
|
|
817
|
+
this._containsFocus = false
|
|
818
|
+
}
|
|
819
|
+
})
|
|
820
|
+
|
|
821
|
+
EventHandler.on(this._element, EVENT_DBLCLICK, event => {
|
|
822
|
+
if (!this._config.editing) {
|
|
823
|
+
return
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const found = this._navigableCellFromTarget(event.target as HTMLElement)
|
|
827
|
+
if (found && found.cell.rowIndex >= 0) {
|
|
828
|
+
this._activeCell?.set(found.cell)
|
|
829
|
+
this._startEditing(found.cell)
|
|
830
|
+
}
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_SORT_BUTTON, event => {
|
|
834
|
+
this._toggleColumnSorting(event)
|
|
835
|
+
})
|
|
836
|
+
|
|
837
|
+
const applyColumnFilter = debounce((key: string, value: string) => {
|
|
838
|
+
if (this._table) {
|
|
839
|
+
this._table.getColumn(key)?.setFilterValue(value === '' ? undefined : value)
|
|
840
|
+
}
|
|
841
|
+
}, FILTER_DEBOUNCE_MS)
|
|
842
|
+
|
|
843
|
+
EventHandler.on(this._element, EVENT_INPUT, SELECTOR_COLUMN_FILTER, event => {
|
|
844
|
+
const input = (event.delegateTarget ?? event.target) as HTMLInputElement
|
|
845
|
+
applyColumnFilter(input.dataset.coreuiColumnFilter!, input.value)
|
|
846
|
+
})
|
|
847
|
+
|
|
848
|
+
const applyGlobalFilter = debounce((value: string) => {
|
|
849
|
+
if (this._table) {
|
|
850
|
+
this._table.setGlobalFilter(value === '' ? undefined : value)
|
|
851
|
+
}
|
|
852
|
+
}, FILTER_DEBOUNCE_MS)
|
|
853
|
+
|
|
854
|
+
EventHandler.on(this._element, EVENT_INPUT, SELECTOR_GLOBAL_FILTER, event => {
|
|
855
|
+
const input = (event.delegateTarget ?? event.target) as HTMLInputElement
|
|
856
|
+
applyGlobalFilter(input.value)
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_MENU_BUTTON, event => {
|
|
860
|
+
const button = (event.delegateTarget ?? event.target) as HTMLElement
|
|
861
|
+
this._menu?.toggle(button, button.dataset.coreuiMenu!)
|
|
862
|
+
})
|
|
863
|
+
|
|
864
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_FILTER_BUTTON, event => {
|
|
865
|
+
const button = (event.delegateTarget ?? event.target) as HTMLElement
|
|
866
|
+
this._filterMenu?.toggle(button, button.dataset.coreuiFilter!)
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_TOOLBAR_COLUMNS, event => {
|
|
870
|
+
const button = (event.delegateTarget ?? event.target) as HTMLElement
|
|
871
|
+
this._columnChooser?.toggle(button)
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_TOOLBAR_EXPORT, () => {
|
|
875
|
+
this.downloadCsv({ scope: 'filtered', ...this._toolbarConfig?.exportOptions })
|
|
876
|
+
})
|
|
877
|
+
|
|
878
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_TOOLBAR_UNDO, () => {
|
|
879
|
+
this.undo()
|
|
880
|
+
})
|
|
881
|
+
|
|
882
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_TOOLBAR_REDO, () => {
|
|
883
|
+
this.redo()
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
EventHandler.on(this._element, EVENT_DRAGSTART, SELECTOR_DRAGGABLE, event => {
|
|
887
|
+
this._onColumnDragStart(event)
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
EventHandler.on(this._element, EVENT_DRAGOVER, SELECTOR_DRAGGABLE, event => {
|
|
891
|
+
if (!this._dragColumn) {
|
|
892
|
+
return
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const target = (event.delegateTarget ?? event.target) as HTMLElement
|
|
896
|
+
const targetKey = target.dataset.coreuiColumn
|
|
897
|
+
// A drop is only valid within the drag source's pinned region; leaving
|
|
898
|
+
// other targets un-prevented shows the browser's no-drop cursor there.
|
|
899
|
+
const source = this._table.getColumn(this._dragColumn)
|
|
900
|
+
const targetColumn = targetKey ? this._table.getColumn(targetKey) : undefined
|
|
901
|
+
if (!source || !targetColumn || targetKey === this._dragColumn || source.getIsPinned() !== targetColumn.getIsPinned()) {
|
|
902
|
+
return
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
event.preventDefault()
|
|
906
|
+
target.classList.add(CLASS_NAME_DROP_TARGET)
|
|
907
|
+
})
|
|
908
|
+
|
|
909
|
+
EventHandler.on(this._element, EVENT_DRAGLEAVE, SELECTOR_DRAGGABLE, event => {
|
|
910
|
+
const target = (event.delegateTarget ?? event.target) as HTMLElement
|
|
911
|
+
target.classList.remove(CLASS_NAME_DROP_TARGET)
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
EventHandler.on(this._element, EVENT_DROP, SELECTOR_DRAGGABLE, event => {
|
|
915
|
+
this._onColumnDrop(event)
|
|
916
|
+
})
|
|
917
|
+
|
|
918
|
+
EventHandler.on(this._element, EVENT_DRAGEND, SELECTOR_DRAGGABLE, () => {
|
|
919
|
+
this._endColumnDrag()
|
|
920
|
+
})
|
|
921
|
+
|
|
922
|
+
EventHandler.on(this._element, EVENT_MOUSEDOWN, SELECTOR_RESIZER, event => {
|
|
923
|
+
this._startColumnResize(event)
|
|
924
|
+
})
|
|
925
|
+
|
|
926
|
+
EventHandler.on(this._element, EVENT_TOUCHSTART, SELECTOR_RESIZER, event => {
|
|
927
|
+
this._startColumnResize(event)
|
|
928
|
+
})
|
|
929
|
+
|
|
930
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_ROW_SELECT, event => {
|
|
931
|
+
this._toggleRowSelection(event)
|
|
932
|
+
})
|
|
933
|
+
|
|
934
|
+
EventHandler.on(this._element, EVENT_CHANGE, SELECTOR_SELECT_ALL, event => {
|
|
935
|
+
const input = (event.delegateTarget ?? event.target) as HTMLInputElement
|
|
936
|
+
this._table.toggleAllRowsSelected(input.checked)
|
|
937
|
+
})
|
|
938
|
+
|
|
939
|
+
EventHandler.on(this._element, EVENT_CLICK, SELECTOR_PAGE, event => {
|
|
940
|
+
const button = (event.delegateTarget ?? event.target) as HTMLButtonElement
|
|
941
|
+
const page = button.dataset.coreuiPage!
|
|
942
|
+
|
|
943
|
+
if (page === 'first') {
|
|
944
|
+
this._table.firstPage()
|
|
945
|
+
return
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (page === 'prev') {
|
|
949
|
+
this._table.previousPage()
|
|
950
|
+
return
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (page === 'next') {
|
|
954
|
+
this._table.nextPage()
|
|
955
|
+
return
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
if (page === 'last') {
|
|
959
|
+
this._table.lastPage()
|
|
960
|
+
return
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
this._table.setPageIndex(Number(page))
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
EventHandler.on(this._element, EVENT_CHANGE, SELECTOR_PAGE_SIZE, event => {
|
|
967
|
+
const select = (event.delegateTarget ?? event.target) as HTMLSelectElement
|
|
968
|
+
this._table.setPageSize(Number(select.value))
|
|
969
|
+
})
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private _toggleColumnSorting(event: Event & Record<string, unknown>): void {
|
|
973
|
+
const button = (event.delegateTarget ?? event.target) as HTMLElement
|
|
974
|
+
const key = button.closest('th')?.dataset.coreuiColumn
|
|
975
|
+
if (!key) {
|
|
976
|
+
return
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const column = this._table.getColumn(key)
|
|
980
|
+
column?.getToggleSortingHandler()?.(event)
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Maps an event target to the navigable cell that holds it: a data cell
|
|
984
|
+
// (rowIndex from the row model) or a header label-row cell (rowIndex -1).
|
|
985
|
+
// Selection cells, the filter row, and grid chrome outside the table map to
|
|
986
|
+
// null.
|
|
987
|
+
private _navigableCellFromTarget(target: HTMLElement): { cellElement: HTMLElement; cell: ActiveCell } | null {
|
|
988
|
+
const cellElement = target.closest<HTMLElement>('td, th')
|
|
989
|
+
if (!cellElement || !this._tableElement.contains(cellElement)) {
|
|
990
|
+
return null
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
const columnId = cellElement.dataset.coreuiColumn
|
|
994
|
+
if (!columnId) {
|
|
995
|
+
return null
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const rowElement = cellElement.closest('tr')!
|
|
999
|
+
|
|
1000
|
+
if (rowElement.parentElement === this._body.element) {
|
|
1001
|
+
const index = rowElement.dataset.coreuiIndex
|
|
1002
|
+
if (index === undefined) {
|
|
1003
|
+
return null
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return { cellElement, cell: { rowIndex: Number(index), columnId } }
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (rowElement === this._header.element.firstElementChild) {
|
|
1010
|
+
return { cellElement, cell: { rowIndex: -1, columnId } }
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return null
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private _onCellFocusIn(event: FocusEvent): void {
|
|
1017
|
+
if (!this._activeCell) {
|
|
1018
|
+
return
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
const found = this._navigableCellFromTarget(event.target as HTMLElement)
|
|
1022
|
+
if (found) {
|
|
1023
|
+
// The browser already moved focus (click, descend) - only sync state.
|
|
1024
|
+
this._activeCell.set(found.cell)
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
private _onCellNavigationKeydown(event: KeyboardEvent): void {
|
|
1029
|
+
if (!this._activeCell) {
|
|
1030
|
+
return
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const target = event.target as HTMLElement
|
|
1034
|
+
|
|
1035
|
+
// Ctrl/Cmd+Z / Ctrl+Shift+Z / Ctrl+Y - edit history (MUI/AG parity).
|
|
1036
|
+
// Not while an editor is open (the input's native undo wins) and not
|
|
1037
|
+
// from form controls (filter/search inputs own their undo).
|
|
1038
|
+
if (
|
|
1039
|
+
this._config.history &&
|
|
1040
|
+
(event.ctrlKey || event.metaKey) &&
|
|
1041
|
+
!this._editingCell &&
|
|
1042
|
+
!target.matches('input, textarea, select')
|
|
1043
|
+
) {
|
|
1044
|
+
const key = event.key.toLowerCase()
|
|
1045
|
+
if (key === 'z' && !event.shiftKey) {
|
|
1046
|
+
event.preventDefault()
|
|
1047
|
+
this.undo()
|
|
1048
|
+
return
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
if ((key === 'z' && event.shiftKey) || key === 'y') {
|
|
1052
|
+
event.preventDefault()
|
|
1053
|
+
this.redo()
|
|
1054
|
+
return
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (this._editingCell && this._editScopeContains(target)) {
|
|
1059
|
+
// A defaultPrevented key was consumed by the editor (a rich editor
|
|
1060
|
+
// closing its own overlay on Escape, an autocomplete accepting on
|
|
1061
|
+
// Enter) - one key, one layer.
|
|
1062
|
+
if (event.key === 'Enter') {
|
|
1063
|
+
if (!event.defaultPrevented) {
|
|
1064
|
+
event.preventDefault()
|
|
1065
|
+
this._stopEditing({ commit: true })
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
return
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (event.key === 'Escape') {
|
|
1072
|
+
if (!event.defaultPrevented) {
|
|
1073
|
+
this._cancelEdit()
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (event.key === 'Tab') {
|
|
1080
|
+
event.preventDefault()
|
|
1081
|
+
this._moveEditing(event.shiftKey ? -1 : 1)
|
|
1082
|
+
return
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Everything else (arrows, typing) stays native inside the editor.
|
|
1086
|
+
return
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// Focus fell back to the viewport after the active row was recycled out
|
|
1090
|
+
// of the window: any navigation key scrolls the active cell back in view
|
|
1091
|
+
// and refocuses it.
|
|
1092
|
+
if (target === this._viewportElement) {
|
|
1093
|
+
const active = this._activeCell.get()
|
|
1094
|
+
if (active && event.key !== 'Tab') {
|
|
1095
|
+
event.preventDefault()
|
|
1096
|
+
this._setActiveCell(active)
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
return
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
const found = this._navigableCellFromTarget(target)
|
|
1103
|
+
if (!found) {
|
|
1104
|
+
return
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
if (target !== found.cellElement) {
|
|
1108
|
+
// Focus is descended into the cell's content; Escape ascends back.
|
|
1109
|
+
if (event.key === 'Escape') {
|
|
1110
|
+
found.cellElement.focus({ preventScroll: true })
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
return
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (event.key === 'Enter' || event.key === 'F2') {
|
|
1117
|
+
event.preventDefault()
|
|
1118
|
+
const config = this._columnsByKey.get(found.cell.columnId)
|
|
1119
|
+
if (found.cell.rowIndex === -1) {
|
|
1120
|
+
if (event.key === 'Enter' && config && this._isColumnSortable(config)) {
|
|
1121
|
+
this._table.getColumn(found.cell.columnId)?.getToggleSortingHandler()?.(event)
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (config && this._isColumnEditable(config)) {
|
|
1128
|
+
this._startEditing(found.cell)
|
|
1129
|
+
return
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
if (event.key === 'Enter') {
|
|
1133
|
+
this._descendIntoCell(found.cellElement)
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
if (event.key === ' ') {
|
|
1140
|
+
if (this._config.rowSelection && found.cell.rowIndex >= 0) {
|
|
1141
|
+
event.preventDefault()
|
|
1142
|
+
const row = this._table.getRowModel().rows[found.cell.rowIndex]
|
|
1143
|
+
row?.toggleSelected()
|
|
1144
|
+
this._selectionAnchorIndex = found.cell.rowIndex
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
return
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const result = resolveNavigation(event.key, event.ctrlKey || event.metaKey, found.cell, this._activeCellContext())
|
|
1151
|
+
if (!result) {
|
|
1152
|
+
return
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
event.preventDefault()
|
|
1156
|
+
|
|
1157
|
+
if (result.pageDelta === 1 && this._table.getCanNextPage()) {
|
|
1158
|
+
this._table.nextPage()
|
|
1159
|
+
} else if (result.pageDelta === -1 && this._table.getCanPreviousPage()) {
|
|
1160
|
+
this._table.previousPage()
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
this._setActiveCell(result.cell)
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private _descendIntoCell(cellElement: HTMLElement): void {
|
|
1167
|
+
cellElement.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)?.focus({ preventScroll: true })
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private _isColumnEditable(column: DataGridColumn): boolean {
|
|
1171
|
+
if (!this._config.editing) {
|
|
1172
|
+
return false
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
// Opt-IN, unlike the opt-out feature booleans - editing is invasive. A
|
|
1176
|
+
// custom editor is itself the opt-in.
|
|
1177
|
+
return Boolean(column.editor) || column.editable === true || typeof column.editable === 'object'
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
private _startEditing(cell: ActiveCell): void {
|
|
1181
|
+
const config = this._columnsByKey.get(cell.columnId)
|
|
1182
|
+
if (!config || !this._isColumnEditable(config)) {
|
|
1183
|
+
return
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
this._stopEditing({ commit: true, force: true })
|
|
1187
|
+
|
|
1188
|
+
const row = this._table.getRowModel().rows[cell.rowIndex]
|
|
1189
|
+
if (!row) {
|
|
1190
|
+
return
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const value = row.getValue(cell.columnId)
|
|
1194
|
+
const context: DataGridEditorContext = {
|
|
1195
|
+
item: row.original,
|
|
1196
|
+
column: config,
|
|
1197
|
+
value,
|
|
1198
|
+
commit: committed => this._commitEdit(committed),
|
|
1199
|
+
cancel: () => this._cancelEdit(),
|
|
1200
|
+
labels: this._labels
|
|
1201
|
+
}
|
|
1202
|
+
const editable: DataGridEditableOptions = typeof config.editable === 'object' ? config.editable : {}
|
|
1203
|
+
const editor = config.editor ? config.editor(context) : createBuiltInEditor(editable, context)
|
|
1204
|
+
|
|
1205
|
+
this._editingCell = {
|
|
1206
|
+
rowId: row.id,
|
|
1207
|
+
columnId: cell.columnId,
|
|
1208
|
+
rowIndex: cell.rowIndex,
|
|
1209
|
+
item: row.original,
|
|
1210
|
+
previousValue: value
|
|
1211
|
+
}
|
|
1212
|
+
this._editor = editor
|
|
1213
|
+
this._editorFocusPending = true
|
|
1214
|
+
|
|
1215
|
+
EventHandler.trigger(this._element, EVENT_EDIT_START, { item: row.original, columnId: cell.columnId })
|
|
1216
|
+
|
|
1217
|
+
if (editor.popup) {
|
|
1218
|
+
this._mountEditorPopup(cell)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Commit on blur (inline editors) and on pointerdown outside the edit
|
|
1222
|
+
// scope (popup editors, portaled overlays) - blur alone cannot commit,
|
|
1223
|
+
// picking a date in a calendar blurs the input mid-edit.
|
|
1224
|
+
this._editorPointerdownHandler = (domEvent: Event) => {
|
|
1225
|
+
if (!this._editScopeContains(domEvent.target as Node)) {
|
|
1226
|
+
this._stopEditing({ commit: true })
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
EventHandler.on(document, EVENT_POINTERDOWN, this._editorPointerdownHandler)
|
|
1231
|
+
EventHandler.on(editor.element, EVENT_FOCUSOUT, (domEvent: Event) => {
|
|
1232
|
+
const related = (domEvent as FocusEvent).relatedTarget as Node | null
|
|
1233
|
+
if (related && !this._editScopeContains(related)) {
|
|
1234
|
+
this._stopEditing({ commit: true })
|
|
1235
|
+
}
|
|
1236
|
+
})
|
|
1237
|
+
|
|
1238
|
+
this._scheduler.schedule()
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// The edit scope: the editing cell, the editor element, its popup layer,
|
|
1242
|
+
// and anything the editor claims via contains() (portaled overlays).
|
|
1243
|
+
private _editScopeContains(target: Node | null): boolean {
|
|
1244
|
+
if (!target || !this._editingCell || !this._editor) {
|
|
1245
|
+
return false
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (this._editor.contains?.(target) || this._editor.element.contains(target) || this._editorPopup?.contains(target)) {
|
|
1249
|
+
return true
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const cellElement = this._cellElementFor({
|
|
1253
|
+
rowIndex: this._editingCell.rowIndex,
|
|
1254
|
+
columnId: this._editingCell.columnId
|
|
1255
|
+
})
|
|
1256
|
+
return Boolean(cellElement?.contains(target))
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
private _mountEditorPopup(cell: ActiveCell): void {
|
|
1260
|
+
const cellElement = this._cellElementFor(cell)
|
|
1261
|
+
const popup = document.createElement('div')
|
|
1262
|
+
popup.classList.add(CLASS_NAME_EDITOR_POPUP)
|
|
1263
|
+
|
|
1264
|
+
if (cellElement) {
|
|
1265
|
+
const rootRect = this._element.getBoundingClientRect()
|
|
1266
|
+
const cellRect = cellElement.getBoundingClientRect()
|
|
1267
|
+
popup.style.top = `${cellRect.top - rootRect.top}px`
|
|
1268
|
+
popup.style.left = `${cellRect.left - rootRect.left}px`
|
|
1269
|
+
popup.style.minWidth = `${cellRect.width}px`
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
popup.append(this._editor!.element)
|
|
1273
|
+
this._element.append(popup)
|
|
1274
|
+
this._editorPopup = popup
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
private _commitEdit(value: unknown): void {
|
|
1278
|
+
const editing = this._editingCell
|
|
1279
|
+
if (!editing) {
|
|
1280
|
+
return
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
const validate = this._columnsByKey.get(editing.columnId)?.editValidate
|
|
1284
|
+
if (validate) {
|
|
1285
|
+
const result = validate(value, editing.item)
|
|
1286
|
+
if (result !== true) {
|
|
1287
|
+
this._showEditorInvalid(typeof result === 'string' ? result : '')
|
|
1288
|
+
return
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
this._teardownEditor()
|
|
1293
|
+
// The grid never mutates items - the app applies the change (setItems /
|
|
1294
|
+
// items prop) and stays the single source of truth.
|
|
1295
|
+
if (this._config.history) {
|
|
1296
|
+
this._undoStack.push({
|
|
1297
|
+
rowId: editing.rowId, columnId: editing.columnId, value, previousValue: editing.previousValue
|
|
1298
|
+
})
|
|
1299
|
+
if (this._undoStack.length > HISTORY_LIMIT) {
|
|
1300
|
+
this._undoStack.shift()
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
this._redoStack = []
|
|
1304
|
+
this._updateHistoryButtons()
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
EventHandler.trigger(this._element, EVENT_EDIT_COMMIT, {
|
|
1308
|
+
item: editing.item,
|
|
1309
|
+
columnId: editing.columnId,
|
|
1310
|
+
value,
|
|
1311
|
+
previousValue: editing.previousValue
|
|
1312
|
+
})
|
|
1313
|
+
this._activeCell?.set({ rowIndex: editing.rowIndex, columnId: editing.columnId }, { focus: true })
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
private _cancelEdit(): void {
|
|
1317
|
+
const editing = this._editingCell
|
|
1318
|
+
if (!editing) {
|
|
1319
|
+
return
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
this._teardownEditor()
|
|
1323
|
+
EventHandler.trigger(this._element, EVENT_EDIT_CANCEL, { item: editing.item, columnId: editing.columnId })
|
|
1324
|
+
this._activeCell?.set({ rowIndex: editing.rowIndex, columnId: editing.columnId }, { focus: true })
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
private _stopEditing(options: { commit: boolean; force?: boolean }): void {
|
|
1328
|
+
if (!this._editingCell) {
|
|
1329
|
+
return
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
if (options.commit && this._editor?.getValue) {
|
|
1333
|
+
this._commitEdit(this._editor.getValue())
|
|
1334
|
+
// An invalid value blocks the commit; when the edit cannot survive
|
|
1335
|
+
// anyway (row recycled away, row set changed), drop the draft.
|
|
1336
|
+
if (this._editingCell && options.force) {
|
|
1337
|
+
this._cancelEdit()
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
return
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// A custom editor without getValue owns its commits - stopping from the
|
|
1344
|
+
// outside can only cancel.
|
|
1345
|
+
this._cancelEdit()
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
private _showEditorInvalid(message: string): void {
|
|
1349
|
+
const editor = this._editor
|
|
1350
|
+
if (!editor) {
|
|
1351
|
+
return
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const input = editor.element.matches('input, select, textarea') ?
|
|
1355
|
+
editor.element :
|
|
1356
|
+
editor.element.querySelector<HTMLElement>('input, select, textarea')
|
|
1357
|
+
if (!input) {
|
|
1358
|
+
return
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
if (!this._editorErrorElement) {
|
|
1362
|
+
this._editorErrorElement = document.createElement('div')
|
|
1363
|
+
this._editorErrorElement.classList.add('visually-hidden')
|
|
1364
|
+
this._editorErrorElement.id = `data-grid-editor-error-${++editorErrorCount}`
|
|
1365
|
+
this._element.append(this._editorErrorElement)
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
this._editorErrorElement.textContent = message
|
|
1369
|
+
input.classList.add('is-invalid')
|
|
1370
|
+
input.setAttribute('aria-invalid', 'true')
|
|
1371
|
+
input.setAttribute('aria-errormessage', this._editorErrorElement.id)
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
private _moveEditing(direction: -1 | 1): void {
|
|
1375
|
+
const editing = this._editingCell
|
|
1376
|
+
if (!editing) {
|
|
1377
|
+
return
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const columns = this._visibleColumns()
|
|
1381
|
+
const currentIndex = columns.findIndex(column => column.key === editing.columnId)
|
|
1382
|
+
let next: DataGridColumn | null = null
|
|
1383
|
+
for (let index = currentIndex + direction; index >= 0 && index < columns.length; index += direction) {
|
|
1384
|
+
if (this._isColumnEditable(columns[index])) {
|
|
1385
|
+
next = columns[index]
|
|
1386
|
+
break
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
this._stopEditing({ commit: true })
|
|
1391
|
+
if (this._editingCell) {
|
|
1392
|
+
// An invalid value blocked the commit - stay.
|
|
1393
|
+
return
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
if (next) {
|
|
1397
|
+
const cell = { rowIndex: editing.rowIndex, columnId: next.key }
|
|
1398
|
+
this._activeCell?.set(cell, { focus: true })
|
|
1399
|
+
this._startEditing(cell)
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
private _teardownEditor(): void {
|
|
1404
|
+
if (this._editorPointerdownHandler) {
|
|
1405
|
+
EventHandler.off(document, EVENT_POINTERDOWN, this._editorPointerdownHandler)
|
|
1406
|
+
this._editorPointerdownHandler = null
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
if (this._editor) {
|
|
1410
|
+
EventHandler.off(this._editor.element, EVENT_FOCUSOUT)
|
|
1411
|
+
this._editor.dispose?.()
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
if (this._editorPopup) {
|
|
1415
|
+
this._editorPopup.remove()
|
|
1416
|
+
this._editorPopup = null
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
if (this._editorErrorElement) {
|
|
1420
|
+
this._editorErrorElement.remove()
|
|
1421
|
+
this._editorErrorElement = null
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
this._editor = null
|
|
1425
|
+
this._editingCell = null
|
|
1426
|
+
this._editorFocusPending = false
|
|
1427
|
+
this._scheduler.schedule()
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
private _setActiveCell(cell: ActiveCell): void {
|
|
1431
|
+
this._activeCell?.set(cell, { focus: true })
|
|
1432
|
+
if (this._virtualizer && cell.rowIndex >= 0) {
|
|
1433
|
+
// No-op when the row is already in view (align: auto); otherwise the
|
|
1434
|
+
// focus request stays pending until the row renders on the next flush.
|
|
1435
|
+
this._virtualizer.instance.scrollToIndex(cell.rowIndex)
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
private _activeCellContext(): ActiveCellContext {
|
|
1440
|
+
const rowCount = this._table.getRowModel().rows.length
|
|
1441
|
+
const paginated = Boolean(this._config.pagination) || Boolean(this._config.dataProvider)
|
|
1442
|
+
const viewportRows = Math.floor((this._viewportElement?.clientHeight ?? 0) / this._config.rowHeight)
|
|
1443
|
+
|
|
1444
|
+
return {
|
|
1445
|
+
columnIds: this._visibleLeafColumns().map(column => column.id),
|
|
1446
|
+
rowCount,
|
|
1447
|
+
pageStep: paginated ?
|
|
1448
|
+
this._table.getState().pagination.pageSize :
|
|
1449
|
+
Math.max(1, viewportRows || rowCount),
|
|
1450
|
+
paginated
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private _cellElementFor(cell: ActiveCell): HTMLElement | null {
|
|
1455
|
+
if (cell.rowIndex === -1) {
|
|
1456
|
+
return this._header.element.querySelector<HTMLElement>(
|
|
1457
|
+
`tr:first-child th[data-coreui-column="${cell.columnId}"]`
|
|
1458
|
+
)
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
const rowElement = this._body.element.querySelector<HTMLElement>(`tr[data-coreui-index="${cell.rowIndex}"]`)
|
|
1462
|
+
return rowElement?.querySelector<HTMLElement>(`td[data-coreui-column="${cell.columnId}"]`) ?? null
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// The roving-tabindex pass: runs after every render/recycle flush, in the
|
|
1466
|
+
// same slot as the aria attributes, so focus is always re-derived from
|
|
1467
|
+
// state (never from what the recycler left in the DOM).
|
|
1468
|
+
private _applyCellNavigation(): void {
|
|
1469
|
+
if (!this._activeCell) {
|
|
1470
|
+
return
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const context = this._activeCellContext()
|
|
1474
|
+
this._activeCell.clamp(context)
|
|
1475
|
+
|
|
1476
|
+
this._tableElement.setAttribute(
|
|
1477
|
+
'aria-colcount',
|
|
1478
|
+
String(context.columnIds.length + (this._config.rowSelection ? 1 : 0))
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1481
|
+
const active = this._activeCell.get()
|
|
1482
|
+
// With no active cell yet, the first data cell (or first header cell in
|
|
1483
|
+
// an empty grid) is the grid's single tab stop.
|
|
1484
|
+
const tabbable = active ?? (context.columnIds.length > 0 ?
|
|
1485
|
+
{ rowIndex: context.rowCount > 0 ? 0 : -1, columnId: context.columnIds[0] } :
|
|
1486
|
+
null)
|
|
1487
|
+
|
|
1488
|
+
for (const cellElement of this._header.element.querySelectorAll<HTMLElement>('tr:first-child th[data-coreui-column]')) {
|
|
1489
|
+
applyTabIndex(
|
|
1490
|
+
cellElement,
|
|
1491
|
+
tabbable !== null && tabbable.rowIndex === -1 && cellElement.dataset.coreuiColumn === tabbable.columnId
|
|
1492
|
+
)
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
for (const rowElement of this._body.element.querySelectorAll<HTMLElement>('tr[data-coreui-index]')) {
|
|
1496
|
+
const isActiveRow = tabbable !== null && rowElement.dataset.coreuiIndex === String(tabbable.rowIndex)
|
|
1497
|
+
for (const cellElement of rowElement.querySelectorAll<HTMLElement>('td[data-coreui-column]')) {
|
|
1498
|
+
applyTabIndex(cellElement, isActiveRow && cellElement.dataset.coreuiColumn === tabbable!.columnId)
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (!active) {
|
|
1503
|
+
return
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const activeElement = this._cellElementFor(active)
|
|
1507
|
+
|
|
1508
|
+
if (activeElement) {
|
|
1509
|
+
if (this._activeCell.consumeFocusRequest()) {
|
|
1510
|
+
activeElement.focus({ preventScroll: true })
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
return
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// The active cell scrolled out of the rendered window while focused: park
|
|
1517
|
+
// focus on the viewport so the keydown fallback stays inside the grid.
|
|
1518
|
+
if (this._containsFocus && !this._element.contains(document.activeElement)) {
|
|
1519
|
+
this._viewportElement.focus({ preventScroll: true })
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
private _getMenuActions(columnKey: string): DataGridMenuAction[] {
|
|
1524
|
+
const column = this._table.getColumn(columnKey)
|
|
1525
|
+
const config = this._columnsByKey.get(columnKey)
|
|
1526
|
+
if (!column || !config) {
|
|
1527
|
+
return []
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
const actions: DataGridMenuAction[] = []
|
|
1531
|
+
|
|
1532
|
+
if (this._isColumnSortable(config)) {
|
|
1533
|
+
const sorted = column.getIsSorted()
|
|
1534
|
+
actions.push(
|
|
1535
|
+
{
|
|
1536
|
+
key: 'sort-asc',
|
|
1537
|
+
label: this._labels.sortAscending,
|
|
1538
|
+
disabled: sorted === 'asc',
|
|
1539
|
+
group: 'sorting',
|
|
1540
|
+
icon: this._sanitizeIcon(this._config.sortAscendingIcon),
|
|
1541
|
+
run: () => column.toggleSorting(false)
|
|
1542
|
+
},
|
|
1543
|
+
{
|
|
1544
|
+
key: 'sort-desc',
|
|
1545
|
+
label: this._labels.sortDescending,
|
|
1546
|
+
disabled: sorted === 'desc',
|
|
1547
|
+
group: 'sorting',
|
|
1548
|
+
icon: this._sanitizeIcon(this._config.sortDescendingIcon),
|
|
1549
|
+
run: () => column.toggleSorting(true)
|
|
1550
|
+
}
|
|
1551
|
+
)
|
|
1552
|
+
|
|
1553
|
+
if (sorted) {
|
|
1554
|
+
actions.push({
|
|
1555
|
+
key: 'clear-sort',
|
|
1556
|
+
label: this._labels.clearSort,
|
|
1557
|
+
group: 'sorting',
|
|
1558
|
+
icon: this._sanitizeIcon(this._config.sortNeutralIcon),
|
|
1559
|
+
run: () => column.clearSorting()
|
|
1560
|
+
})
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
if (this._config.columnPinning) {
|
|
1565
|
+
const pinned = column.getIsPinned()
|
|
1566
|
+
if (pinned !== 'left') {
|
|
1567
|
+
actions.push({
|
|
1568
|
+
key: 'pin-left',
|
|
1569
|
+
label: this._labels.pinLeft,
|
|
1570
|
+
group: 'pinning',
|
|
1571
|
+
icon: this._sanitizeIcon(this._config.pinLeftIcon),
|
|
1572
|
+
run: () => column.pin('left')
|
|
1573
|
+
})
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
if (pinned !== 'right') {
|
|
1577
|
+
actions.push({
|
|
1578
|
+
key: 'pin-right',
|
|
1579
|
+
label: this._labels.pinRight,
|
|
1580
|
+
group: 'pinning',
|
|
1581
|
+
icon: this._sanitizeIcon(this._config.pinRightIcon),
|
|
1582
|
+
run: () => column.pin('right')
|
|
1583
|
+
})
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
if (pinned) {
|
|
1587
|
+
actions.push({
|
|
1588
|
+
key: 'unpin',
|
|
1589
|
+
label: this._labels.unpin,
|
|
1590
|
+
group: 'pinning',
|
|
1591
|
+
icon: this._sanitizeIcon(this._config.unpinIcon),
|
|
1592
|
+
run: () => column.pin(false)
|
|
1593
|
+
})
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (this._config.columnOrder && config.movable !== false) {
|
|
1598
|
+
const region = this._visibleLeafColumns().filter(item => item.getIsPinned() === column.getIsPinned())
|
|
1599
|
+
const index = region.findIndex(item => item.id === columnKey)
|
|
1600
|
+
actions.push(
|
|
1601
|
+
{
|
|
1602
|
+
key: 'move-left',
|
|
1603
|
+
label: this._labels.moveLeft,
|
|
1604
|
+
disabled: index <= 0,
|
|
1605
|
+
group: 'ordering',
|
|
1606
|
+
icon: this._sanitizeIcon(this._config.moveLeftIcon),
|
|
1607
|
+
run: () => this._moveColumn(columnKey, -1)
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
key: 'move-right',
|
|
1611
|
+
label: this._labels.moveRight,
|
|
1612
|
+
disabled: index === region.length - 1,
|
|
1613
|
+
group: 'ordering',
|
|
1614
|
+
icon: this._sanitizeIcon(this._config.moveRightIcon),
|
|
1615
|
+
run: () => this._moveColumn(columnKey, 1)
|
|
1616
|
+
}
|
|
1617
|
+
)
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
if (this._config.columnVisibility && column.getCanHide()) {
|
|
1621
|
+
actions.push({
|
|
1622
|
+
key: 'hide',
|
|
1623
|
+
label: this._labels.hideColumn,
|
|
1624
|
+
group: 'visibility',
|
|
1625
|
+
icon: this._sanitizeIcon(this._config.hideColumnIcon),
|
|
1626
|
+
run: () => column.toggleVisibility(false)
|
|
1627
|
+
})
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// A builder receives the built-in actions (icons already sanitized) and the
|
|
1631
|
+
// column, and returns the final list — reorder, filter, or add custom items.
|
|
1632
|
+
const builder = typeof this._config.columnMenu === 'function' ? this._config.columnMenu : null
|
|
1633
|
+
const finalActions = builder ? builder({ column: config, defaultActions: actions }) : actions
|
|
1634
|
+
|
|
1635
|
+
// Sanitize again so custom items' icons pass through the SVG allow list too
|
|
1636
|
+
// (re-sanitizing the built-in icons is a no-op).
|
|
1637
|
+
return finalActions.map(action =>
|
|
1638
|
+
(action.icon ? { ...action, icon: this._sanitizeIcon(action.icon) } : action)
|
|
1639
|
+
)
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// All leaf ids in the current order, hidden columns included - building a new
|
|
1643
|
+
// order from visible ids only would drop hidden columns, and TanStack appends
|
|
1644
|
+
// missing ids at the end when they are shown again.
|
|
1645
|
+
private _orderedLeafIds(): string[] {
|
|
1646
|
+
const allIds = this._table.getAllLeafColumns().map(column => column.id)
|
|
1647
|
+
const order = this._table.getState().columnOrder
|
|
1648
|
+
if (order.length === 0) {
|
|
1649
|
+
return allIds
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const known = new Set(allIds)
|
|
1653
|
+
const ordered = order.filter(id => known.has(id))
|
|
1654
|
+
const seen = new Set(ordered)
|
|
1655
|
+
return [...ordered, ...allIds.filter(id => !seen.has(id))]
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
private _moveColumn(columnKey: string, direction: -1 | 1): void {
|
|
1659
|
+
const column = this._table.getColumn(columnKey)
|
|
1660
|
+
if (!column) {
|
|
1661
|
+
return
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
const region = column.getIsPinned()
|
|
1665
|
+
const ids = this._orderedLeafIds()
|
|
1666
|
+
const regionIds = this._visibleLeafColumns()
|
|
1667
|
+
.filter(item => item.getIsPinned() === region)
|
|
1668
|
+
.map(item => item.id)
|
|
1669
|
+
const neighbor = regionIds[regionIds.indexOf(columnKey) + direction]
|
|
1670
|
+
if (!neighbor) {
|
|
1671
|
+
return
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
const from = ids.indexOf(columnKey)
|
|
1675
|
+
const to = ids.indexOf(neighbor)
|
|
1676
|
+
ids[from] = neighbor
|
|
1677
|
+
ids[to] = columnKey
|
|
1678
|
+
this._table.setColumnOrder(ids)
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
private _onColumnDragStart(event: Event & Record<string, unknown>): void {
|
|
1682
|
+
// A drag started from the resize handle is a resize, not a reorder. The
|
|
1683
|
+
// handle's mousedown is preventDefault'd so the browser never starts a drag
|
|
1684
|
+
// there; this ancestry check is a secondary guard.
|
|
1685
|
+
if ((event.target as HTMLElement).closest?.(SELECTOR_RESIZER)) {
|
|
1686
|
+
event.preventDefault()
|
|
1687
|
+
return
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
const target = (event.delegateTarget ?? event.target) as HTMLElement
|
|
1691
|
+
this._dragColumn = target.dataset.coreuiColumn ?? null
|
|
1692
|
+
target.classList.add(CLASS_NAME_DRAGGING)
|
|
1693
|
+
|
|
1694
|
+
const { dataTransfer } = event as unknown as DragEvent
|
|
1695
|
+
if (dataTransfer) {
|
|
1696
|
+
dataTransfer.effectAllowed = 'move'
|
|
1697
|
+
dataTransfer.setData('text/plain', this._dragColumn ?? '')
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
private _onColumnDrop(event: Event & Record<string, unknown>): void {
|
|
1702
|
+
event.preventDefault()
|
|
1703
|
+
const target = (event.delegateTarget ?? event.target) as HTMLElement
|
|
1704
|
+
const targetKey = target.dataset.coreuiColumn
|
|
1705
|
+
const sourceKey = this._dragColumn
|
|
1706
|
+
this._endColumnDrag()
|
|
1707
|
+
|
|
1708
|
+
if (!sourceKey || !targetKey || sourceKey === targetKey) {
|
|
1709
|
+
return
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
const source = this._table.getColumn(sourceKey)
|
|
1713
|
+
const targetColumn = this._table.getColumn(targetKey)
|
|
1714
|
+
// Reordering never crosses a pinning boundary — pin membership is state,
|
|
1715
|
+
// not position.
|
|
1716
|
+
if (!source || !targetColumn || source.getIsPinned() !== targetColumn.getIsPinned()) {
|
|
1717
|
+
return
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
const ids = this._orderedLeafIds()
|
|
1721
|
+
const from = ids.indexOf(sourceKey)
|
|
1722
|
+
const to = ids.indexOf(targetKey)
|
|
1723
|
+
if (from === -1 || to === -1) {
|
|
1724
|
+
return
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
// Removing the source first makes inserting at the target's original index
|
|
1728
|
+
// land after it when dragging right and before it when dragging left.
|
|
1729
|
+
ids.splice(from, 1)
|
|
1730
|
+
ids.splice(to, 0, sourceKey)
|
|
1731
|
+
this._table.setColumnOrder(ids)
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
private _endColumnDrag(): void {
|
|
1735
|
+
this._dragColumn = null
|
|
1736
|
+
for (const cell of this._element.querySelectorAll(`.${CLASS_NAME_DRAGGING}, .${CLASS_NAME_DROP_TARGET}`)) {
|
|
1737
|
+
cell.classList.remove(CLASS_NAME_DRAGGING, CLASS_NAME_DROP_TARGET)
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
private _startColumnResize(event: Event & Record<string, unknown>): void {
|
|
1742
|
+
const target = (event.delegateTarget ?? event.target) as HTMLElement
|
|
1743
|
+
const key = target.closest('th')?.dataset.coreuiColumn
|
|
1744
|
+
if (!key) {
|
|
1745
|
+
return
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// Stop the browser from starting a native column-reorder drag from the
|
|
1749
|
+
// draggable th when the press lands on the resize handle inside it.
|
|
1750
|
+
if (event.type === 'mousedown') {
|
|
1751
|
+
event.preventDefault()
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
const header = this._table.getHeaderGroups()[0]?.headers.find(item => item.column.id === key)
|
|
1755
|
+
header?.getResizeHandler()(event)
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
private _toggleRowSelection(event: Event & Record<string, unknown>): void {
|
|
1759
|
+
const input = (event.delegateTarget ?? event.target) as HTMLInputElement
|
|
1760
|
+
const rowElement = input.closest('tr')
|
|
1761
|
+
if (!rowElement) {
|
|
1762
|
+
return
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
const index = Number(rowElement.dataset.coreuiIndex)
|
|
1766
|
+
const { rows } = this._table.getRowModel()
|
|
1767
|
+
const selected = input.checked
|
|
1768
|
+
|
|
1769
|
+
if ((event as unknown as MouseEvent).shiftKey && this._selectionAnchorIndex !== null) {
|
|
1770
|
+
const start = Math.min(this._selectionAnchorIndex, index)
|
|
1771
|
+
const end = Math.max(this._selectionAnchorIndex, index)
|
|
1772
|
+
this._table.setRowSelection(previous => {
|
|
1773
|
+
const next = { ...previous }
|
|
1774
|
+
for (const row of rows.slice(start, end + 1)) {
|
|
1775
|
+
if (selected) {
|
|
1776
|
+
next[row.id] = true
|
|
1777
|
+
} else {
|
|
1778
|
+
delete next[row.id]
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
return next
|
|
1783
|
+
})
|
|
1784
|
+
} else {
|
|
1785
|
+
rows[index]?.toggleSelected(selected)
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
this._selectionAnchorIndex = index
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
private _initDataSource(): void {
|
|
1792
|
+
const { dataProvider } = this._config
|
|
1793
|
+
if (!dataProvider) {
|
|
1794
|
+
return
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
this._dataSource = createDataSource({
|
|
1798
|
+
provider: request => dataProvider(request),
|
|
1799
|
+
getRequest: () => {
|
|
1800
|
+
const state = this._table.getState()
|
|
1801
|
+
return {
|
|
1802
|
+
columnFilters: state.columnFilters,
|
|
1803
|
+
globalFilter: (state.globalFilter as string | undefined) ?? '',
|
|
1804
|
+
pagination: state.pagination,
|
|
1805
|
+
sorting: state.sorting
|
|
1806
|
+
}
|
|
1807
|
+
},
|
|
1808
|
+
onLoad: response => {
|
|
1809
|
+
this._stopEditing({ commit: false })
|
|
1810
|
+
this._loadFailed = false
|
|
1811
|
+
this._dataVersion++
|
|
1812
|
+
this._table.setOptions(previous => ({
|
|
1813
|
+
...previous,
|
|
1814
|
+
data: response.items,
|
|
1815
|
+
rowCount: response.totalRows
|
|
1816
|
+
}))
|
|
1817
|
+
EventHandler.trigger(this._element, EVENT_DATA_LOAD, {
|
|
1818
|
+
items: response.items,
|
|
1819
|
+
totalRows: response.totalRows
|
|
1820
|
+
})
|
|
1821
|
+
this._announceResults(response.totalRows)
|
|
1822
|
+
this._scheduler.schedule()
|
|
1823
|
+
},
|
|
1824
|
+
onError: error => {
|
|
1825
|
+
this._stopEditing({ commit: false })
|
|
1826
|
+
this._loadFailed = true
|
|
1827
|
+
this._dataVersion++
|
|
1828
|
+
this._table.setOptions(previous => ({ ...previous, data: [], rowCount: 0 }))
|
|
1829
|
+
EventHandler.trigger(this._element, EVENT_DATA_ERROR, { error })
|
|
1830
|
+
this._scheduler.schedule()
|
|
1831
|
+
},
|
|
1832
|
+
onLoadingChange: loading => {
|
|
1833
|
+
if (this._loading) {
|
|
1834
|
+
this._loading.setVisible(loading)
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
this._viewportElement.setAttribute('aria-busy', String(loading))
|
|
1838
|
+
}
|
|
1839
|
+
})
|
|
1840
|
+
|
|
1841
|
+
this._dataSource.load()
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
// Visible leaf columns in VISUAL order: left-pinned, center (columnOrder),
|
|
1845
|
+
// right-pinned — getVisibleLeafColumns() alone does not regroup pinning.
|
|
1846
|
+
private _visibleLeafColumns() {
|
|
1847
|
+
return [
|
|
1848
|
+
...this._table.getLeftVisibleLeafColumns(),
|
|
1849
|
+
...this._table.getCenterVisibleLeafColumns(),
|
|
1850
|
+
...this._table.getRightVisibleLeafColumns()
|
|
1851
|
+
]
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
private _chooserColumns(): ColumnChooserColumn[] {
|
|
1855
|
+
const columns: ColumnChooserColumn[] = []
|
|
1856
|
+
for (const id of this._orderedLeafIds()) {
|
|
1857
|
+
const column = this._table.getColumn(id)
|
|
1858
|
+
const config = this._columnsByKey.get(id)
|
|
1859
|
+
if (!column || !config) {
|
|
1860
|
+
continue
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
columns.push({
|
|
1864
|
+
id,
|
|
1865
|
+
label: config.label ?? config.key,
|
|
1866
|
+
canHide: column.getCanHide(),
|
|
1867
|
+
visible: column.getIsVisible()
|
|
1868
|
+
})
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
return columns
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
private _visibleColumns(): DataGridColumn[] {
|
|
1875
|
+
const columns: DataGridColumn[] = []
|
|
1876
|
+
for (const column of this._visibleLeafColumns()) {
|
|
1877
|
+
const config = this._columnsByKey.get(column.id)
|
|
1878
|
+
if (config) {
|
|
1879
|
+
columns.push(config)
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
return columns
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
private _getColumnSizes(): Record<string, number> {
|
|
1887
|
+
const sizes: Record<string, number> = {}
|
|
1888
|
+
for (const header of this._table.getHeaderGroups()[0]?.headers ?? []) {
|
|
1889
|
+
sizes[header.column.id] = header.getSize()
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
return sizes
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
private _getFilteredColumns(): string[] {
|
|
1896
|
+
return this._table.getState().columnFilters.map(filter => filter.id)
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
private _getPinning(): GridPinning {
|
|
1900
|
+
// Visible columns only - getLeftLeafColumns/getPinnedIndex include hidden
|
|
1901
|
+
// columns, which would leave the edge class on a column nobody sees.
|
|
1902
|
+
const leftColumns = this._table.getLeftLeafColumns().filter(column => column.getIsVisible())
|
|
1903
|
+
const rightColumns = this._table.getRightLeafColumns().filter(column => column.getIsVisible())
|
|
1904
|
+
// The selection cell is not a TanStack column; anchor it to the left as
|
|
1905
|
+
// soon as any data column is pinned there so the checkbox travels with them.
|
|
1906
|
+
const selectPinned = Boolean(this._config.rowSelection) && leftColumns.length > 0
|
|
1907
|
+
const columns: Record<string, PinnedCellInfo> = {}
|
|
1908
|
+
|
|
1909
|
+
for (const [index, column] of leftColumns.entries()) {
|
|
1910
|
+
columns[column.id] = {
|
|
1911
|
+
side: 'left',
|
|
1912
|
+
offset: column.getStart('left'),
|
|
1913
|
+
edge: index === leftColumns.length - 1,
|
|
1914
|
+
afterSelect: selectPinned
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
for (const [index, column] of rightColumns.entries()) {
|
|
1919
|
+
columns[column.id] = {
|
|
1920
|
+
side: 'right',
|
|
1921
|
+
offset: column.getAfter('right'),
|
|
1922
|
+
edge: index === 0,
|
|
1923
|
+
afterSelect: false
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
return {
|
|
1928
|
+
columns,
|
|
1929
|
+
select: selectPinned ?
|
|
1930
|
+
{
|
|
1931
|
+
side: 'left', offset: 0, edge: false, afterSelect: false
|
|
1932
|
+
} :
|
|
1933
|
+
null
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
private _storageKey(): string | null {
|
|
1938
|
+
return this._config.stateKey ? `${STATE_STORAGE_PREFIX}${this._config.stateKey}` : null
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
private _restorePersistedState(): void {
|
|
1942
|
+
const key = this._storageKey()
|
|
1943
|
+
if (!key) {
|
|
1944
|
+
return
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
let state: Partial<DataGridState> | null = null
|
|
1948
|
+
try {
|
|
1949
|
+
const raw = window.localStorage.getItem(key)
|
|
1950
|
+
state = raw ? (JSON.parse(raw) as Partial<DataGridState>) : null
|
|
1951
|
+
} catch {
|
|
1952
|
+
return
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
if (!state) {
|
|
1956
|
+
return
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
this.restoreState(state)
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
private _updateHistoryButtons(): void {
|
|
1963
|
+
const undoButton = this._element.querySelector<HTMLButtonElement>(SELECTOR_TOOLBAR_UNDO)
|
|
1964
|
+
if (undoButton) {
|
|
1965
|
+
undoButton.disabled = this._undoStack.length === 0
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
const redoButton = this._element.querySelector<HTMLButtonElement>(SELECTOR_TOOLBAR_REDO)
|
|
1969
|
+
if (redoButton) {
|
|
1970
|
+
redoButton.disabled = this._redoStack.length === 0
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
private _announceResults(count: number): void {
|
|
1975
|
+
this._announce(interpolate(this._labels.resultsAnnouncement, { count }))
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
private _announce(message: string): void {
|
|
1979
|
+
if (this._liveRegion) {
|
|
1980
|
+
this._liveRegion.textContent = message
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
private _getTotalRows(): number {
|
|
1985
|
+
if (this._config.dataProvider) {
|
|
1986
|
+
return this._table.options.rowCount ?? 0
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
return this._table.getFilteredRowModel().rows.length
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
private _notifyStateChange(previous: TableState, next: TableState): void {
|
|
1993
|
+
// TanStack's resize handler keeps document-level listeners until mouseup;
|
|
1994
|
+
// a dispose mid-drag would re-enter here against a torn-down instance.
|
|
1995
|
+
if (!this._element) {
|
|
1996
|
+
return
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// stateKey persistence is automatic - every state change (debounced)
|
|
2000
|
+
// rewrites the snapshot, so the view survives a reload with no explicit
|
|
2001
|
+
// save step.
|
|
2002
|
+
if (this._config.stateKey) {
|
|
2003
|
+
if (!this._persistState) {
|
|
2004
|
+
this._persistState = debounce(() => {
|
|
2005
|
+
const key = this._storageKey()
|
|
2006
|
+
if (!key) {
|
|
2007
|
+
return
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
try {
|
|
2011
|
+
window.localStorage.setItem(key, JSON.stringify(this.getState()))
|
|
2012
|
+
} catch {
|
|
2013
|
+
// Quota/privacy-mode failures degrade to a non-persistent grid.
|
|
2014
|
+
}
|
|
2015
|
+
}, STATE_AUTOSAVE_DEBOUNCE_MS)
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
this._persistState()
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
// The shift-click range anchor and the active cell are indexes into the
|
|
2022
|
+
// current row set; any transition that reshuffles rows would make them
|
|
2023
|
+
// point at arbitrary ones. An in-flight edit stops (commit) first.
|
|
2024
|
+
// Pagination compares by VALUE: a data replacement makes TanStack
|
|
2025
|
+
// auto-reset the page index, which replaces the state object without
|
|
2026
|
+
// changing it - identity alone would kill an edit right after its
|
|
2027
|
+
// editCommit handler applied the change.
|
|
2028
|
+
if (
|
|
2029
|
+
previous.sorting !== next.sorting ||
|
|
2030
|
+
previous.columnFilters !== next.columnFilters ||
|
|
2031
|
+
previous.globalFilter !== next.globalFilter ||
|
|
2032
|
+
previous.pagination.pageIndex !== next.pagination.pageIndex ||
|
|
2033
|
+
previous.pagination.pageSize !== next.pagination.pageSize
|
|
2034
|
+
) {
|
|
2035
|
+
this._stopEditing({ commit: true, force: true })
|
|
2036
|
+
this._selectionAnchorIndex = null
|
|
2037
|
+
this._activeCell?.reset()
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
if (previous.sorting !== next.sorting) {
|
|
2041
|
+
EventHandler.trigger(this._element, EVENT_SORTING_CHANGE, {
|
|
2042
|
+
sorting: next.sorting
|
|
2043
|
+
})
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
const filtersChanged =
|
|
2047
|
+
previous.columnFilters !== next.columnFilters || previous.globalFilter !== next.globalFilter
|
|
2048
|
+
|
|
2049
|
+
if (filtersChanged) {
|
|
2050
|
+
EventHandler.trigger(this._element, EVENT_FILTER_CHANGE, {
|
|
2051
|
+
columnFilters: next.columnFilters,
|
|
2052
|
+
globalFilter: next.globalFilter ?? ''
|
|
2053
|
+
})
|
|
2054
|
+
|
|
2055
|
+
if (!this._config.dataProvider) {
|
|
2056
|
+
this._announceResults(this._table.getFilteredRowModel().rows.length)
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
if (previous.rowSelection !== next.rowSelection) {
|
|
2061
|
+
EventHandler.trigger(this._element, EVENT_SELECTION_CHANGE, {
|
|
2062
|
+
rowSelection: next.rowSelection,
|
|
2063
|
+
selectedItems: this.getSelectedItems()
|
|
2064
|
+
})
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
if (previous.pagination !== next.pagination) {
|
|
2068
|
+
EventHandler.trigger(this._element, EVENT_PAGINATION_CHANGE, {
|
|
2069
|
+
pagination: next.pagination
|
|
2070
|
+
})
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
if (previous.columnSizing !== next.columnSizing) {
|
|
2074
|
+
EventHandler.trigger(this._element, EVENT_SIZING_CHANGE, {
|
|
2075
|
+
columnSizing: next.columnSizing
|
|
2076
|
+
})
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
if (previous.columnPinning !== next.columnPinning) {
|
|
2080
|
+
EventHandler.trigger(this._element, EVENT_PINNING_CHANGE, {
|
|
2081
|
+
columnPinning: next.columnPinning
|
|
2082
|
+
})
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
if (previous.columnOrder !== next.columnOrder) {
|
|
2086
|
+
EventHandler.trigger(this._element, EVENT_ORDER_CHANGE, {
|
|
2087
|
+
columnOrder: next.columnOrder
|
|
2088
|
+
})
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (previous.columnVisibility !== next.columnVisibility) {
|
|
2092
|
+
EventHandler.trigger(this._element, EVENT_VISIBILITY_CHANGE, {
|
|
2093
|
+
columnVisibility: next.columnVisibility
|
|
2094
|
+
})
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
if (this._dataSource) {
|
|
2098
|
+
const refetchNeeded = filtersChanged || previous.sorting !== next.sorting
|
|
2099
|
+
|
|
2100
|
+
if (refetchNeeded && next.pagination.pageIndex !== 0) {
|
|
2101
|
+
// Back to page one first; the resulting pagination change re-enters
|
|
2102
|
+
// this handler and triggers the (debounced) fetch exactly once.
|
|
2103
|
+
this._table.setPageIndex(0)
|
|
2104
|
+
return
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
if (refetchNeeded || previous.pagination !== next.pagination) {
|
|
2108
|
+
this._dataSource.load()
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
private _update(): void {
|
|
2114
|
+
if (this._virtualizer) {
|
|
2115
|
+
this._virtualizer.setCount(this._table.getRowModel().rows.length)
|
|
2116
|
+
this._virtualizer.instance._willUpdate()
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// Scrolling the editing row out of the rendered window commits the edit
|
|
2120
|
+
// (AG behavior) - BEFORE the recycler wipes the editor's DOM.
|
|
2121
|
+
if (this._editingCell && this._virtualizer) {
|
|
2122
|
+
const { rowIndex } = this._editingCell
|
|
2123
|
+
const rendered = this._virtualizer.instance.getVirtualItems().some(item => item.index === rowIndex)
|
|
2124
|
+
if (!rendered) {
|
|
2125
|
+
this._stopEditing({ commit: true, force: true })
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
this._tableElement.setAttribute(
|
|
2130
|
+
'aria-rowcount',
|
|
2131
|
+
String(this._header.rowCount + this._getTotalRows())
|
|
2132
|
+
)
|
|
2133
|
+
|
|
2134
|
+
this._header.update({
|
|
2135
|
+
filteredColumns: this._config.columnFilters ? this._getFilteredColumns() : null,
|
|
2136
|
+
order: this._visibleLeafColumns().map(column => column.id),
|
|
2137
|
+
pinning: this._config.columnPinning ? this._getPinning() : null,
|
|
2138
|
+
resizingColumn: this._config.columnSizing ?
|
|
2139
|
+
this._table.getState().columnSizingInfo.isResizingColumn :
|
|
2140
|
+
false,
|
|
2141
|
+
sizes: (this._config.columnSizing || this._config.columnPinning) ? this._getColumnSizes() : null,
|
|
2142
|
+
sorting: this._table.getState().sorting,
|
|
2143
|
+
selectAll: this._config.rowSelection ?
|
|
2144
|
+
{
|
|
2145
|
+
checked: this._table.getIsAllRowsSelected(),
|
|
2146
|
+
indeterminate: this._table.getIsSomeRowsSelected()
|
|
2147
|
+
} :
|
|
2148
|
+
null
|
|
2149
|
+
})
|
|
2150
|
+
this._body.update()
|
|
2151
|
+
this._applyCellNavigation()
|
|
2152
|
+
|
|
2153
|
+
// The editor element enters the DOM during the body pass (inline) or at
|
|
2154
|
+
// mount (popup) - focus it once it is connected.
|
|
2155
|
+
if (this._editorFocusPending && this._editor?.element.isConnected) {
|
|
2156
|
+
this._editorFocusPending = false
|
|
2157
|
+
if (this._editor.focus) {
|
|
2158
|
+
this._editor.focus()
|
|
2159
|
+
} else {
|
|
2160
|
+
(this._editor.element.querySelector<HTMLElement>(FOCUSABLE_SELECTOR) ?? this._editor.element).focus({
|
|
2161
|
+
preventScroll: true
|
|
2162
|
+
})
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
this._toolbar?.update?.()
|
|
2167
|
+
this._emptySlot?.update?.()
|
|
2168
|
+
|
|
2169
|
+
for (const instance of this._paginations) {
|
|
2170
|
+
instance.refresh()
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2174
|
+
private _paginationState() {
|
|
2175
|
+
const { pagination } = this._table.getState()
|
|
2176
|
+
return {
|
|
2177
|
+
pageCount: this._table.getPageCount(),
|
|
2178
|
+
pageIndex: pagination.pageIndex,
|
|
2179
|
+
pageSize: pagination.pageSize,
|
|
2180
|
+
totalRows: this._getTotalRows()
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// Static
|
|
2185
|
+
static jQueryInterface(
|
|
2186
|
+
this: { each: (callback: (this: HTMLElement) => void) => unknown },
|
|
2187
|
+
config?: string | Partial<DataGridOptions>
|
|
2188
|
+
) {
|
|
2189
|
+
return this.each(function () {
|
|
2190
|
+
const data = DataGrid.getOrCreateInstance(this, typeof config === 'string' ? undefined : config)
|
|
2191
|
+
|
|
2192
|
+
if (typeof config !== 'string') {
|
|
2193
|
+
return
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
const methods = data as unknown as Record<string, (() => void) | undefined>
|
|
2197
|
+
if (methods[config] === undefined || config.startsWith('_') || config === 'constructor') {
|
|
2198
|
+
throw new TypeError(`No method named "${config}"`)
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
methods[config]!()
|
|
2202
|
+
})
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
defineJQueryPlugin(DataGrid)
|
|
2207
|
+
|
|
2208
|
+
export default DataGrid
|