@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/ui/header.ts
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid ui/header.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { interpolate } from '../labels'
|
|
9
|
+
import type {
|
|
10
|
+
DataGridColumn, DataGridItem, DataGridLabels, DataGridSlot, SortingState, Table
|
|
11
|
+
} from '../types'
|
|
12
|
+
import { applyPinnedCell, type GridPinning } from './pinning'
|
|
13
|
+
|
|
14
|
+
const CLASS_NAME_HEADER = 'data-grid-header'
|
|
15
|
+
const CLASS_NAME_HEADER_CELL = 'data-grid-header-cell'
|
|
16
|
+
const CLASS_NAME_HEADER_LABEL = 'data-grid-header-label'
|
|
17
|
+
const CLASS_NAME_FILTER_ROW = 'data-grid-filter-row'
|
|
18
|
+
const CLASS_NAME_RESIZER = 'data-grid-resizer'
|
|
19
|
+
const CLASS_NAME_RESIZER_ACTIVE = 'data-grid-resizer-active'
|
|
20
|
+
const CLASS_NAME_SELECT_CELL = 'data-grid-select-cell'
|
|
21
|
+
const CLASS_NAME_SORTABLE = 'data-grid-sortable'
|
|
22
|
+
const CLASS_NAME_SORT_BUTTON = 'data-grid-sort-button'
|
|
23
|
+
const CLASS_NAME_SORT_ICON = 'data-grid-sort-icon'
|
|
24
|
+
|
|
25
|
+
export interface GridHeaderContext {
|
|
26
|
+
// With cellNavigation on, the label row joins the roving-tabindex grid: its
|
|
27
|
+
// cells carry grid roles and neither they nor their buttons are tabbable.
|
|
28
|
+
// The filter row keeps native tab semantics (an input row, not grid cells).
|
|
29
|
+
cellNavigation: boolean
|
|
30
|
+
columns: DataGridColumn[]
|
|
31
|
+
draggable: boolean
|
|
32
|
+
filterRow: boolean
|
|
33
|
+
hasMenu: (column: DataGridColumn) => boolean
|
|
34
|
+
icons: {
|
|
35
|
+
columnMenu: string
|
|
36
|
+
filter: string
|
|
37
|
+
sortAscending: string
|
|
38
|
+
sortDescending: string
|
|
39
|
+
sortNeutral: string
|
|
40
|
+
}
|
|
41
|
+
hasFilterMenu: (column: DataGridColumn) => boolean
|
|
42
|
+
isFilterable: (column: DataGridColumn) => boolean
|
|
43
|
+
isResizable: (column: DataGridColumn) => boolean
|
|
44
|
+
isSortable: (column: DataGridColumn) => boolean
|
|
45
|
+
labels: DataGridLabels
|
|
46
|
+
resizable: boolean
|
|
47
|
+
selection: boolean
|
|
48
|
+
selectAll: boolean
|
|
49
|
+
table: Table<DataGridItem>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface GridHeaderState {
|
|
53
|
+
// Columns with any active filter value - drives the filter button indicator.
|
|
54
|
+
// Null when column filters are off.
|
|
55
|
+
filteredColumns: string[] | null
|
|
56
|
+
// Visible leaf column ids in table order (pinning + columnOrder + visibility).
|
|
57
|
+
order: string[]
|
|
58
|
+
pinning: GridPinning | null
|
|
59
|
+
resizingColumn: string | false
|
|
60
|
+
sizes: Record<string, number> | null
|
|
61
|
+
sorting: SortingState
|
|
62
|
+
selectAll: {
|
|
63
|
+
checked: boolean
|
|
64
|
+
indeterminate: boolean
|
|
65
|
+
} | null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface GridHeader {
|
|
69
|
+
element: HTMLTableSectionElement
|
|
70
|
+
rowCount: number
|
|
71
|
+
update: (state: GridHeaderState) => void
|
|
72
|
+
dispose: () => void
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type SortDirection = 'ascending' | 'descending' | 'neutral'
|
|
76
|
+
|
|
77
|
+
const sortDirection = (columnSort: SortingState[number] | undefined): SortDirection => {
|
|
78
|
+
if (!columnSort) {
|
|
79
|
+
return 'neutral'
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return columnSort.desc ? 'descending' : 'ascending'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const iconForDirection = (direction: SortDirection, icons: GridHeaderContext['icons']): string => {
|
|
86
|
+
if (direction === 'ascending') {
|
|
87
|
+
return icons.sortAscending
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (direction === 'descending') {
|
|
91
|
+
return icons.sortDescending
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return icons.sortNeutral
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const createSelectAllCell = (context: GridHeaderContext): { cell: HTMLTableCellElement; input: HTMLInputElement | null } => {
|
|
98
|
+
const cell = document.createElement('th')
|
|
99
|
+
cell.classList.add(CLASS_NAME_SELECT_CELL)
|
|
100
|
+
cell.setAttribute('scope', 'col')
|
|
101
|
+
|
|
102
|
+
if (!context.selectAll) {
|
|
103
|
+
return { cell, input: null }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const input = document.createElement('input')
|
|
107
|
+
input.type = 'checkbox'
|
|
108
|
+
input.classList.add('form-check-input')
|
|
109
|
+
input.setAttribute('aria-label', context.labels.selectAllRows)
|
|
110
|
+
input.dataset.coreuiSelectAll = ''
|
|
111
|
+
cell.append(input)
|
|
112
|
+
|
|
113
|
+
return { cell, input }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const createResizer = (): HTMLElement => {
|
|
117
|
+
const resizer = document.createElement('span')
|
|
118
|
+
resizer.classList.add(CLASS_NAME_RESIZER)
|
|
119
|
+
resizer.setAttribute('aria-hidden', 'true')
|
|
120
|
+
resizer.dataset.coreuiResizer = ''
|
|
121
|
+
return resizer
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const createLabelRow = (
|
|
125
|
+
context: GridHeaderContext,
|
|
126
|
+
cells: Map<string, HTMLTableCellElement>,
|
|
127
|
+
resizers: Map<string, HTMLElement>,
|
|
128
|
+
sortIcons: Map<string, HTMLElement>
|
|
129
|
+
): {
|
|
130
|
+
row: HTMLTableRowElement
|
|
131
|
+
selectAllInput: HTMLInputElement | null
|
|
132
|
+
selectCell: HTMLTableCellElement | null
|
|
133
|
+
} => {
|
|
134
|
+
const row = document.createElement('tr')
|
|
135
|
+
let selectAllInput: HTMLInputElement | null = null
|
|
136
|
+
let selectCell: HTMLTableCellElement | null = null
|
|
137
|
+
|
|
138
|
+
if (context.cellNavigation) {
|
|
139
|
+
row.setAttribute('role', 'row')
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (context.selection) {
|
|
143
|
+
const { cell, input } = createSelectAllCell(context)
|
|
144
|
+
selectAllInput = input
|
|
145
|
+
selectCell = cell
|
|
146
|
+
if (context.cellNavigation) {
|
|
147
|
+
cell.setAttribute('role', 'columnheader')
|
|
148
|
+
if (input) {
|
|
149
|
+
input.tabIndex = -1
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
row.append(cell)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const column of context.columns) {
|
|
157
|
+
const cell = document.createElement('th')
|
|
158
|
+
cell.setAttribute('scope', 'col')
|
|
159
|
+
cell.dataset.coreuiColumn = column.key
|
|
160
|
+
if (context.cellNavigation) {
|
|
161
|
+
cell.setAttribute('role', 'columnheader')
|
|
162
|
+
cell.tabIndex = -1
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (column.style) {
|
|
166
|
+
Object.assign(cell.style, column.style)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (column.width !== undefined) {
|
|
170
|
+
cell.style.width = `${column.width}px`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const content = document.createElement('div')
|
|
174
|
+
content.classList.add(CLASS_NAME_HEADER_CELL)
|
|
175
|
+
|
|
176
|
+
if (context.isSortable(column)) {
|
|
177
|
+
cell.classList.add(CLASS_NAME_SORTABLE)
|
|
178
|
+
const button = document.createElement('button')
|
|
179
|
+
button.type = 'button'
|
|
180
|
+
button.classList.add(CLASS_NAME_SORT_BUTTON)
|
|
181
|
+
button.append(column.label ?? column.key)
|
|
182
|
+
const icon = document.createElement('span')
|
|
183
|
+
icon.classList.add(CLASS_NAME_SORT_ICON)
|
|
184
|
+
icon.setAttribute('aria-hidden', 'true')
|
|
185
|
+
icon.innerHTML = context.icons.sortNeutral
|
|
186
|
+
icon.dataset.coreuiSort = 'neutral'
|
|
187
|
+
button.append(icon)
|
|
188
|
+
sortIcons.set(column.key, icon)
|
|
189
|
+
content.append(button)
|
|
190
|
+
} else {
|
|
191
|
+
const label = document.createElement('span')
|
|
192
|
+
label.classList.add(CLASS_NAME_HEADER_LABEL)
|
|
193
|
+
label.textContent = column.label ?? column.key
|
|
194
|
+
content.append(label)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (context.hasFilterMenu(column)) {
|
|
198
|
+
const button = document.createElement('button')
|
|
199
|
+
button.type = 'button'
|
|
200
|
+
button.classList.add('btn', 'btn-ghost', 'btn-sm', 'data-grid-filter-button')
|
|
201
|
+
button.innerHTML = context.icons.filter
|
|
202
|
+
button.dataset.coreuiFilter = column.key
|
|
203
|
+
button.setAttribute('aria-haspopup', 'dialog')
|
|
204
|
+
button.setAttribute('aria-expanded', 'false')
|
|
205
|
+
const filterLabel = interpolate(context.labels.filterColumn, { column: column.label ?? column.key })
|
|
206
|
+
button.setAttribute('aria-label', filterLabel)
|
|
207
|
+
button.setAttribute('data-coreui-tooltip', filterLabel)
|
|
208
|
+
content.append(button)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (context.hasMenu(column)) {
|
|
212
|
+
const button = document.createElement('button')
|
|
213
|
+
button.type = 'button'
|
|
214
|
+
button.classList.add('btn', 'btn-ghost', 'btn-sm', 'data-grid-menu-button')
|
|
215
|
+
button.innerHTML = context.icons.columnMenu
|
|
216
|
+
button.dataset.coreuiMenu = column.key
|
|
217
|
+
button.setAttribute('aria-haspopup', 'menu')
|
|
218
|
+
button.setAttribute('aria-expanded', 'false')
|
|
219
|
+
const menuLabel = interpolate(context.labels.columnMenu, { column: column.label ?? column.key })
|
|
220
|
+
button.setAttribute('aria-label', menuLabel)
|
|
221
|
+
button.setAttribute('data-coreui-tooltip', menuLabel)
|
|
222
|
+
content.append(button)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
cell.append(content)
|
|
226
|
+
|
|
227
|
+
if (context.cellNavigation) {
|
|
228
|
+
for (const button of content.querySelectorAll('button')) {
|
|
229
|
+
button.tabIndex = -1
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (context.resizable && context.isResizable(column)) {
|
|
234
|
+
const resizer = createResizer()
|
|
235
|
+
resizers.set(column.key, resizer)
|
|
236
|
+
cell.append(resizer)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
cells.set(column.key, cell)
|
|
240
|
+
row.append(cell)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { row, selectAllInput, selectCell }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const createFilterRow = (
|
|
247
|
+
context: GridHeaderContext,
|
|
248
|
+
filterCells: Map<string, HTMLTableCellElement>,
|
|
249
|
+
filterSlots: DataGridSlot[]
|
|
250
|
+
): { row: HTMLTableRowElement; selectCell: HTMLTableCellElement | null } => {
|
|
251
|
+
const row = document.createElement('tr')
|
|
252
|
+
row.classList.add(CLASS_NAME_FILTER_ROW)
|
|
253
|
+
if (context.cellNavigation) {
|
|
254
|
+
row.setAttribute('role', 'row')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let selectCell: HTMLTableCellElement | null = null
|
|
258
|
+
|
|
259
|
+
if (context.selection) {
|
|
260
|
+
selectCell = document.createElement('th')
|
|
261
|
+
row.append(selectCell)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const column of context.columns) {
|
|
265
|
+
const cell = document.createElement('th')
|
|
266
|
+
const tableColumn = context.table.getColumn(column.key)
|
|
267
|
+
if (context.isFilterable(column) && column.filter && tableColumn) {
|
|
268
|
+
const slot = column.filter({ column: tableColumn, table: context.table, labels: context.labels })
|
|
269
|
+
filterSlots.push(slot)
|
|
270
|
+
cell.append(slot.element)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
filterCells.set(column.key, cell)
|
|
274
|
+
row.append(cell)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return { row, selectCell }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const createHeader = (context: GridHeaderContext): GridHeader => {
|
|
281
|
+
const element = document.createElement('thead')
|
|
282
|
+
element.classList.add(CLASS_NAME_HEADER)
|
|
283
|
+
|
|
284
|
+
const cells = new Map<string, HTMLTableCellElement>()
|
|
285
|
+
const resizers = new Map<string, HTMLElement>()
|
|
286
|
+
const sortIcons = new Map<string, HTMLElement>()
|
|
287
|
+
const filterCells = new Map<string, HTMLTableCellElement>()
|
|
288
|
+
const filterSlots: DataGridSlot[] = []
|
|
289
|
+
const movableColumns = new Map(context.columns.map(column => [column.key, column.movable !== false]))
|
|
290
|
+
|
|
291
|
+
const { row, selectAllInput, selectCell } = createLabelRow(context, cells, resizers, sortIcons)
|
|
292
|
+
element.append(row)
|
|
293
|
+
|
|
294
|
+
let filterRowElement: HTMLTableRowElement | null = null
|
|
295
|
+
let filterSelectCell: HTMLTableCellElement | null = null
|
|
296
|
+
if (context.filterRow) {
|
|
297
|
+
const filterRow = createFilterRow(context, filterCells, filterSlots)
|
|
298
|
+
filterSelectCell = filterRow.selectCell
|
|
299
|
+
filterRowElement = filterRow.row
|
|
300
|
+
element.append(filterRow.row)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Cells are created once per config column; ordering/visibility re-appends
|
|
304
|
+
// the existing nodes (appending moves them), so sort buttons, resizers and
|
|
305
|
+
// custom filter UIs keep their DOM state.
|
|
306
|
+
let orderSignature: string | null = null
|
|
307
|
+
const applyOrder = (order: string[]) => {
|
|
308
|
+
const visible = new Set(order)
|
|
309
|
+
const reorderRow = (
|
|
310
|
+
rowElement: HTMLTableRowElement,
|
|
311
|
+
cellMap: Map<string, HTMLTableCellElement>,
|
|
312
|
+
leading: HTMLTableCellElement | null
|
|
313
|
+
) => {
|
|
314
|
+
const ordered: HTMLTableCellElement[] = leading ? [leading] : []
|
|
315
|
+
for (const id of order) {
|
|
316
|
+
const cell = cellMap.get(id)
|
|
317
|
+
if (cell) {
|
|
318
|
+
ordered.push(cell)
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
rowElement.append(...ordered)
|
|
323
|
+
if (context.cellNavigation) {
|
|
324
|
+
for (const [index, cell] of ordered.entries()) {
|
|
325
|
+
cell.setAttribute('aria-colindex', String(index + 1))
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
for (const [key, cell] of cellMap) {
|
|
330
|
+
if (!visible.has(key)) {
|
|
331
|
+
cell.remove()
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
reorderRow(row, cells, selectCell)
|
|
337
|
+
if (filterRowElement) {
|
|
338
|
+
reorderRow(filterRowElement, filterCells, filterSelectCell)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
element,
|
|
344
|
+
rowCount: context.filterRow ? 2 : 1,
|
|
345
|
+
update(state) {
|
|
346
|
+
const signature = state.order.join('|')
|
|
347
|
+
if (signature !== orderSignature) {
|
|
348
|
+
orderSignature = signature
|
|
349
|
+
applyOrder(state.order)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
for (const [key, cell] of cells) {
|
|
353
|
+
const columnSort = state.sorting.find(sort => sort.id === key)
|
|
354
|
+
const direction = sortDirection(columnSort)
|
|
355
|
+
if (direction === 'neutral') {
|
|
356
|
+
cell.removeAttribute('aria-sort')
|
|
357
|
+
} else {
|
|
358
|
+
cell.setAttribute('aria-sort', direction)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const sortIcon = sortIcons.get(key)
|
|
362
|
+
if (sortIcon && sortIcon.dataset.coreuiSort !== direction) {
|
|
363
|
+
sortIcon.dataset.coreuiSort = direction
|
|
364
|
+
sortIcon.innerHTML = iconForDirection(direction, context.icons)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (state.sizes && state.sizes[key] !== undefined) {
|
|
368
|
+
cell.style.width = `${state.sizes[key]}px`
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (state.pinning) {
|
|
372
|
+
applyPinnedCell(cell, state.pinning.columns[key] ?? null)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// A column is draggable only when reordering is on and its pinned region
|
|
377
|
+
// holds another movable column to swap with - reordering never crosses a
|
|
378
|
+
// pinning boundary, so a column alone in its region has no valid target.
|
|
379
|
+
if (context.draggable) {
|
|
380
|
+
const regionOf = (key: string): string => state.pinning?.columns[key]?.side ?? 'center'
|
|
381
|
+
const movablePerRegion = new Map<string, number>()
|
|
382
|
+
for (const key of state.order) {
|
|
383
|
+
if (movableColumns.get(key)) {
|
|
384
|
+
const region = regionOf(key)
|
|
385
|
+
movablePerRegion.set(region, (movablePerRegion.get(region) ?? 0) + 1)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
for (const [key, cell] of cells) {
|
|
390
|
+
const draggable = Boolean(movableColumns.get(key)) && (movablePerRegion.get(regionOf(key)) ?? 0) > 1
|
|
391
|
+
cell.draggable = draggable
|
|
392
|
+
if (draggable) {
|
|
393
|
+
cell.dataset.coreuiDraggable = ''
|
|
394
|
+
} else {
|
|
395
|
+
delete cell.dataset.coreuiDraggable
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
for (const [key, resizer] of resizers) {
|
|
401
|
+
resizer.classList.toggle(CLASS_NAME_RESIZER_ACTIVE, state.resizingColumn === key)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (state.filteredColumns) {
|
|
405
|
+
for (const [key, cell] of cells) {
|
|
406
|
+
cell
|
|
407
|
+
.querySelector('[data-coreui-filter]')
|
|
408
|
+
?.classList.toggle('data-grid-filter-button-filtered', state.filteredColumns.includes(key))
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
if (state.pinning) {
|
|
413
|
+
for (const [key, cell] of filterCells) {
|
|
414
|
+
applyPinnedCell(cell, state.pinning.columns[key] ?? null)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (selectCell) {
|
|
418
|
+
applyPinnedCell(selectCell, state.pinning.select)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (filterSelectCell) {
|
|
422
|
+
applyPinnedCell(filterSelectCell, state.pinning.select)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (selectAllInput && state.selectAll) {
|
|
427
|
+
selectAllInput.checked = state.selectAll.checked
|
|
428
|
+
selectAllInput.indeterminate = state.selectAll.indeterminate
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
for (const slot of filterSlots) {
|
|
432
|
+
slot.update?.()
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
dispose() {
|
|
436
|
+
for (const slot of filterSlots) {
|
|
437
|
+
slot.dispose?.()
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid ui/loading.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DataGridLabels } from '../types'
|
|
9
|
+
|
|
10
|
+
const CLASS_NAME_LOADING = 'data-grid-loading'
|
|
11
|
+
|
|
12
|
+
export interface GridLoading {
|
|
13
|
+
element: HTMLDivElement
|
|
14
|
+
setVisible: (visible: boolean) => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const createLoading = (labels: DataGridLabels): GridLoading => {
|
|
18
|
+
const element = document.createElement('div')
|
|
19
|
+
element.classList.add(CLASS_NAME_LOADING)
|
|
20
|
+
element.hidden = true
|
|
21
|
+
|
|
22
|
+
const spinner = document.createElement('div')
|
|
23
|
+
spinner.classList.add('spinner-border')
|
|
24
|
+
spinner.setAttribute('role', 'status')
|
|
25
|
+
|
|
26
|
+
const label = document.createElement('span')
|
|
27
|
+
label.classList.add('visually-hidden')
|
|
28
|
+
label.textContent = labels.loading
|
|
29
|
+
spinner.append(label)
|
|
30
|
+
|
|
31
|
+
element.append(spinner)
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
element,
|
|
35
|
+
setVisible(visible) {
|
|
36
|
+
element.hidden = !visible
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/ui/menu.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid ui/menu.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { DataGridMenuAction } from '../types'
|
|
9
|
+
|
|
10
|
+
const CLASS_NAME_MENU = 'data-grid-menu'
|
|
11
|
+
const CLASS_NAME_MENU_ITEM = 'data-grid-menu-item'
|
|
12
|
+
const CLASS_NAME_MENU_ITEM_ICON = 'data-grid-menu-item-icon'
|
|
13
|
+
const CLASS_NAME_MENU_DIVIDER = 'data-grid-menu-divider'
|
|
14
|
+
|
|
15
|
+
export interface GridMenuContext {
|
|
16
|
+
root: HTMLElement
|
|
17
|
+
getActions: (columnKey: string) => DataGridMenuAction[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GridMenu {
|
|
21
|
+
toggle: (button: HTMLElement, columnKey: string) => void
|
|
22
|
+
close: () => void
|
|
23
|
+
dispose: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let menuCount = 0
|
|
27
|
+
|
|
28
|
+
export const createMenu = (context: GridMenuContext): GridMenu => {
|
|
29
|
+
const element = document.createElement('div')
|
|
30
|
+
element.classList.add(CLASS_NAME_MENU)
|
|
31
|
+
element.setAttribute('role', 'menu')
|
|
32
|
+
element.id = `data-grid-menu-${++menuCount}`
|
|
33
|
+
|
|
34
|
+
let trigger: HTMLElement | null = null
|
|
35
|
+
|
|
36
|
+
const items = (): HTMLButtonElement[] =>
|
|
37
|
+
[...element.querySelectorAll<HTMLButtonElement>(`.${CLASS_NAME_MENU_ITEM}`)]
|
|
38
|
+
|
|
39
|
+
const close = (refocus = false) => {
|
|
40
|
+
if (!trigger) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
trigger.setAttribute('aria-expanded', 'false')
|
|
45
|
+
trigger.removeAttribute('aria-controls')
|
|
46
|
+
if (refocus) {
|
|
47
|
+
trigger.focus()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
trigger = null
|
|
51
|
+
element.remove()
|
|
52
|
+
document.removeEventListener('pointerdown', onOutsidePointerDown, true)
|
|
53
|
+
window.removeEventListener('scroll', onWindowChange, true)
|
|
54
|
+
window.removeEventListener('resize', onWindowChange)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const onOutsidePointerDown = (event: Event) => {
|
|
58
|
+
const target = event.target as Node
|
|
59
|
+
if (!element.contains(target) && !trigger?.contains(target)) {
|
|
60
|
+
close()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Any scroll (viewport or page) or resize invalidates the anchored position;
|
|
65
|
+
// closing matches the native-select/AG Grid behavior.
|
|
66
|
+
const onWindowChange = (event?: Event) => {
|
|
67
|
+
if (event?.target instanceof Node && element.contains(event.target)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
close()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const onMenuKeydown = (event: KeyboardEvent) => {
|
|
75
|
+
const enabled = items()
|
|
76
|
+
const activeIndex = enabled.indexOf(document.activeElement as HTMLButtonElement)
|
|
77
|
+
|
|
78
|
+
switch (event.key) {
|
|
79
|
+
case 'Escape': {
|
|
80
|
+
event.stopPropagation()
|
|
81
|
+
close(true)
|
|
82
|
+
break
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
case 'Tab': {
|
|
86
|
+
close()
|
|
87
|
+
break
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case 'ArrowDown': {
|
|
91
|
+
event.preventDefault()
|
|
92
|
+
enabled[(activeIndex + 1) % enabled.length]?.focus()
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case 'ArrowUp': {
|
|
97
|
+
event.preventDefault()
|
|
98
|
+
enabled[(activeIndex - 1 + enabled.length) % enabled.length]?.focus()
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
case 'Home': {
|
|
103
|
+
event.preventDefault()
|
|
104
|
+
enabled[0]?.focus()
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'End': {
|
|
109
|
+
event.preventDefault()
|
|
110
|
+
enabled[enabled.length - 1]?.focus()
|
|
111
|
+
break
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
default:
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
element.addEventListener('keydown', onMenuKeydown)
|
|
119
|
+
|
|
120
|
+
const open = (button: HTMLElement, columnKey: string) => {
|
|
121
|
+
const actions = context.getActions(columnKey)
|
|
122
|
+
if (actions.length === 0) {
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const nodes: Node[] = []
|
|
127
|
+
for (const [index, action] of actions.entries()) {
|
|
128
|
+
if (index > 0 && action.group !== actions[index - 1].group) {
|
|
129
|
+
const divider = document.createElement('div')
|
|
130
|
+
divider.classList.add(CLASS_NAME_MENU_DIVIDER)
|
|
131
|
+
divider.setAttribute('role', 'separator')
|
|
132
|
+
nodes.push(divider)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const item = document.createElement('button')
|
|
136
|
+
item.type = 'button'
|
|
137
|
+
item.classList.add(CLASS_NAME_MENU_ITEM)
|
|
138
|
+
item.setAttribute('role', 'menuitem')
|
|
139
|
+
if (action.icon) {
|
|
140
|
+
const icon = document.createElement('span')
|
|
141
|
+
icon.classList.add(CLASS_NAME_MENU_ITEM_ICON)
|
|
142
|
+
icon.setAttribute('aria-hidden', 'true')
|
|
143
|
+
icon.innerHTML = action.icon
|
|
144
|
+
item.append(icon)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
item.append(action.label)
|
|
148
|
+
if (action.disabled) {
|
|
149
|
+
item.setAttribute('aria-disabled', 'true')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
item.addEventListener('click', () => {
|
|
153
|
+
if (action.disabled) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const previousTrigger = trigger
|
|
158
|
+
close()
|
|
159
|
+
action.run()
|
|
160
|
+
// The action may remove the trigger's header cell (hide column); the
|
|
161
|
+
// check has to run after the scheduled re-render.
|
|
162
|
+
requestAnimationFrame(() => {
|
|
163
|
+
// An action may have moved focus deliberately (the filter dialog);
|
|
164
|
+
// only restore it when it fell out of the grid.
|
|
165
|
+
const active = document.activeElement
|
|
166
|
+
if (active && active !== document.body && context.root.contains(active)) {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (previousTrigger?.isConnected) {
|
|
171
|
+
previousTrigger.focus()
|
|
172
|
+
} else {
|
|
173
|
+
context.root.querySelector<HTMLElement>('[data-coreui-menu]')?.focus()
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
nodes.push(item)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
element.replaceChildren(...nodes)
|
|
181
|
+
|
|
182
|
+
trigger = button
|
|
183
|
+
button.setAttribute('aria-expanded', 'true')
|
|
184
|
+
button.setAttribute('aria-controls', element.id)
|
|
185
|
+
context.root.append(element)
|
|
186
|
+
|
|
187
|
+
const rootRect = context.root.getBoundingClientRect()
|
|
188
|
+
const buttonRect = button.getBoundingClientRect()
|
|
189
|
+
element.style.top = `${buttonRect.bottom - rootRect.top}px`
|
|
190
|
+
// Right-align the menu to the trigger so it opens leftward — the menu button
|
|
191
|
+
// sits at the cell's right edge, where a left-aligned menu would overflow.
|
|
192
|
+
element.style.left = `${Math.max(buttonRect.right - rootRect.left - element.offsetWidth, 0)}px`
|
|
193
|
+
|
|
194
|
+
document.addEventListener('pointerdown', onOutsidePointerDown, true)
|
|
195
|
+
window.addEventListener('scroll', onWindowChange, true)
|
|
196
|
+
window.addEventListener('resize', onWindowChange)
|
|
197
|
+
items()[0]?.focus()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
toggle(button, columnKey) {
|
|
202
|
+
if (trigger === button) {
|
|
203
|
+
close(true)
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
close()
|
|
208
|
+
open(button, columnKey)
|
|
209
|
+
},
|
|
210
|
+
close() {
|
|
211
|
+
close()
|
|
212
|
+
},
|
|
213
|
+
dispose() {
|
|
214
|
+
close()
|
|
215
|
+
element.removeEventListener('keydown', onMenuKeydown)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|