@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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid ui/pagination.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { interpolate } from '../labels'
|
|
9
|
+
import type { DataGridLabels, DataGridPaginationInfoState } from '../types'
|
|
10
|
+
|
|
11
|
+
const CLASS_NAME_PAGINATION = 'data-grid-pagination'
|
|
12
|
+
const CLASS_NAME_PAGINATION_INFO = 'data-grid-pagination-info'
|
|
13
|
+
|
|
14
|
+
const PAGE_WINDOW = 5
|
|
15
|
+
|
|
16
|
+
export interface GridPaginationContext {
|
|
17
|
+
info: boolean | ((state: DataGridPaginationInfoState) => string)
|
|
18
|
+
labels: DataGridLabels
|
|
19
|
+
pageSizeOptions: number[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GridPagination {
|
|
23
|
+
element: HTMLDivElement
|
|
24
|
+
update: (state: DataGridPaginationInfoState) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const createPageItem = (
|
|
28
|
+
label: string,
|
|
29
|
+
page: string,
|
|
30
|
+
options: { active?: boolean; disabled?: boolean; ariaLabel?: string } = {}
|
|
31
|
+
): HTMLLIElement => {
|
|
32
|
+
const item = document.createElement('li')
|
|
33
|
+
item.classList.add('page-item')
|
|
34
|
+
if (options.active) {
|
|
35
|
+
item.classList.add('active')
|
|
36
|
+
item.setAttribute('aria-current', 'page')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.disabled) {
|
|
40
|
+
item.classList.add('disabled')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const button = document.createElement('button')
|
|
44
|
+
button.type = 'button'
|
|
45
|
+
button.classList.add('page-link')
|
|
46
|
+
button.textContent = label
|
|
47
|
+
button.disabled = Boolean(options.disabled)
|
|
48
|
+
button.dataset.coreuiPage = page
|
|
49
|
+
if (options.ariaLabel) {
|
|
50
|
+
button.setAttribute('aria-label', options.ariaLabel)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
item.append(button)
|
|
54
|
+
|
|
55
|
+
return item
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const createPagination = (context: GridPaginationContext): GridPagination => {
|
|
59
|
+
const element = document.createElement('div')
|
|
60
|
+
element.classList.add(CLASS_NAME_PAGINATION)
|
|
61
|
+
|
|
62
|
+
const nav = document.createElement('nav')
|
|
63
|
+
nav.setAttribute('aria-label', context.labels.paginationLabel)
|
|
64
|
+
const list = document.createElement('ul')
|
|
65
|
+
list.classList.add('pagination', 'pagination-sm')
|
|
66
|
+
nav.append(list)
|
|
67
|
+
|
|
68
|
+
const info = context.info === false ? null : document.createElement('span')
|
|
69
|
+
if (info) {
|
|
70
|
+
info.classList.add(CLASS_NAME_PAGINATION_INFO)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const select = document.createElement('select')
|
|
74
|
+
select.classList.add('form-select', 'form-select-sm')
|
|
75
|
+
select.setAttribute('aria-label', context.labels.pageSizeLabel)
|
|
76
|
+
select.dataset.coreuiPageSize = ''
|
|
77
|
+
for (const size of context.pageSizeOptions) {
|
|
78
|
+
const option = document.createElement('option')
|
|
79
|
+
option.value = String(size)
|
|
80
|
+
option.textContent = String(size)
|
|
81
|
+
select.append(option)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
element.append(nav, ...(info ? [info] : []), select)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
element,
|
|
88
|
+
update(state) {
|
|
89
|
+
const lastPageIndex = Math.max(state.pageCount - 1, 0)
|
|
90
|
+
const windowStart = Math.max(
|
|
91
|
+
Math.min(state.pageIndex - Math.floor(PAGE_WINDOW / 2), state.pageCount - PAGE_WINDOW),
|
|
92
|
+
0
|
|
93
|
+
)
|
|
94
|
+
// No pages (server mode before the first load, rejected provider) - no
|
|
95
|
+
// page buttons, only the disabled first/prev/next/last chrome.
|
|
96
|
+
const windowEnd = state.pageCount === 0 ? -1 : Math.min(windowStart + PAGE_WINDOW - 1, lastPageIndex)
|
|
97
|
+
|
|
98
|
+
const items = [
|
|
99
|
+
createPageItem('«', 'first', {
|
|
100
|
+
ariaLabel: context.labels.firstPage,
|
|
101
|
+
disabled: state.pageIndex === 0
|
|
102
|
+
}),
|
|
103
|
+
createPageItem('‹', 'prev', {
|
|
104
|
+
ariaLabel: context.labels.previousPage,
|
|
105
|
+
disabled: state.pageIndex === 0
|
|
106
|
+
})
|
|
107
|
+
]
|
|
108
|
+
for (let page = windowStart; page <= windowEnd; page++) {
|
|
109
|
+
items.push(createPageItem(String(page + 1), String(page), { active: page === state.pageIndex }))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
items.push(
|
|
113
|
+
createPageItem('›', 'next', {
|
|
114
|
+
ariaLabel: context.labels.nextPage,
|
|
115
|
+
disabled: state.pageIndex >= lastPageIndex
|
|
116
|
+
}),
|
|
117
|
+
createPageItem('»', 'last', {
|
|
118
|
+
ariaLabel: context.labels.lastPage,
|
|
119
|
+
disabled: state.pageIndex >= lastPageIndex
|
|
120
|
+
})
|
|
121
|
+
)
|
|
122
|
+
list.replaceChildren(...items)
|
|
123
|
+
|
|
124
|
+
if (info) {
|
|
125
|
+
if (typeof context.info === 'function') {
|
|
126
|
+
info.textContent = context.info(state)
|
|
127
|
+
} else {
|
|
128
|
+
const first = state.totalRows === 0 ? 0 : (state.pageIndex * state.pageSize) + 1
|
|
129
|
+
const last = Math.min((state.pageIndex + 1) * state.pageSize, state.totalRows)
|
|
130
|
+
info.textContent = interpolate(context.labels.itemsInfo, {
|
|
131
|
+
first,
|
|
132
|
+
last,
|
|
133
|
+
total: state.totalRows
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// A configured pageSize outside pageSizeOptions would leave the select
|
|
139
|
+
// blank; surface it as an extra option in numeric position.
|
|
140
|
+
if (![...select.options].some(option => option.value === String(state.pageSize))) {
|
|
141
|
+
const option = document.createElement('option')
|
|
142
|
+
option.value = String(state.pageSize)
|
|
143
|
+
option.textContent = String(state.pageSize)
|
|
144
|
+
select.insertBefore(option, [...select.options].find(item => Number(item.value) > state.pageSize) ?? null)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
select.value = String(state.pageSize)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid ui/pinning.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CLASS_LEFT = 'data-grid-cell-pinned-left'
|
|
9
|
+
const CLASS_RIGHT = 'data-grid-cell-pinned-right'
|
|
10
|
+
const CLASS_LEFT_LAST = 'data-grid-cell-pinned-left-last'
|
|
11
|
+
const CLASS_RIGHT_FIRST = 'data-grid-cell-pinned-right-first'
|
|
12
|
+
|
|
13
|
+
export interface PinnedCellInfo {
|
|
14
|
+
side: 'left' | 'right'
|
|
15
|
+
offset: number
|
|
16
|
+
edge: boolean
|
|
17
|
+
// A left offset that has to clear the (sticky) selection cell; expressed as a
|
|
18
|
+
// CSS calc so the select-cell width stays a single source of truth in CSS.
|
|
19
|
+
afterSelect: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface GridPinning {
|
|
23
|
+
columns: Record<string, PinnedCellInfo>
|
|
24
|
+
select: PinnedCellInfo | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const applyPinnedCell = (cell: HTMLTableCellElement, info: PinnedCellInfo | null): void => {
|
|
28
|
+
cell.classList.remove(CLASS_LEFT, CLASS_RIGHT, CLASS_LEFT_LAST, CLASS_RIGHT_FIRST)
|
|
29
|
+
cell.style.left = ''
|
|
30
|
+
cell.style.right = ''
|
|
31
|
+
|
|
32
|
+
if (!info) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (info.side === 'left') {
|
|
37
|
+
cell.classList.add(CLASS_LEFT)
|
|
38
|
+
if (info.edge) {
|
|
39
|
+
cell.classList.add(CLASS_LEFT_LAST)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
cell.style.left = info.afterSelect ?
|
|
43
|
+
`calc(var(--cui-data-grid-select-cell-width) + ${info.offset}px)` :
|
|
44
|
+
`${info.offset}px`
|
|
45
|
+
} else {
|
|
46
|
+
cell.classList.add(CLASS_RIGHT)
|
|
47
|
+
if (info.edge) {
|
|
48
|
+
cell.classList.add(CLASS_RIGHT_FIRST)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
cell.style.right = `${info.offset}px`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid ui/toolbar.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CsvDownloadOptions } from '../util/csv'
|
|
9
|
+
import type { DataGridLabels, DataGridOptions } from '../types'
|
|
10
|
+
|
|
11
|
+
const CLASS_NAME_TOOLBAR = 'data-grid-toolbar'
|
|
12
|
+
const CLASS_NAME_ACTIONS = 'data-grid-toolbar-actions'
|
|
13
|
+
const CLASS_NAME_SEARCH = 'data-grid-toolbar-search'
|
|
14
|
+
|
|
15
|
+
export interface GridToolbarContext {
|
|
16
|
+
labels: DataGridLabels
|
|
17
|
+
columns: boolean
|
|
18
|
+
export: boolean
|
|
19
|
+
// Undo/redo of edit commits - both buttons start disabled, the grid
|
|
20
|
+
// toggles them as the history stacks fill.
|
|
21
|
+
history: boolean
|
|
22
|
+
search: boolean
|
|
23
|
+
icons: {
|
|
24
|
+
columns: string
|
|
25
|
+
export: string
|
|
26
|
+
redo: string
|
|
27
|
+
undo: string
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface GridToolbar {
|
|
32
|
+
element: HTMLDivElement
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ResolvedToolbar {
|
|
36
|
+
columns: boolean
|
|
37
|
+
export: boolean
|
|
38
|
+
exportOptions: CsvDownloadOptions
|
|
39
|
+
history: boolean
|
|
40
|
+
search: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Normalizes `toolbar` + `globalFilter` into the individual actions. `toolbar:
|
|
44
|
+
// true` enables each action whose underlying feature is on; a granular object
|
|
45
|
+
// picks individually. `globalFilter: true` stays shorthand for the search input.
|
|
46
|
+
export const resolveToolbar = (options: DataGridOptions): ResolvedToolbar => {
|
|
47
|
+
const { toolbar } = options
|
|
48
|
+
const columnVisibility = Boolean(options.columnVisibility)
|
|
49
|
+
|
|
50
|
+
if (toolbar === true) {
|
|
51
|
+
return {
|
|
52
|
+
columns: columnVisibility,
|
|
53
|
+
export: true,
|
|
54
|
+
exportOptions: {},
|
|
55
|
+
history: options.history,
|
|
56
|
+
search: true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (typeof toolbar === 'object') {
|
|
61
|
+
const exportOption = toolbar.export ?? false
|
|
62
|
+
return {
|
|
63
|
+
columns: (toolbar.columns ?? false) && columnVisibility,
|
|
64
|
+
export: Boolean(exportOption),
|
|
65
|
+
exportOptions: typeof exportOption === 'object' ? exportOption : {},
|
|
66
|
+
history: (toolbar.history ?? false) && options.history,
|
|
67
|
+
search: (toolbar.search ?? false) || Boolean(options.globalFilter)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
columns: false,
|
|
73
|
+
export: false,
|
|
74
|
+
exportOptions: {},
|
|
75
|
+
history: false,
|
|
76
|
+
search: Boolean(options.globalFilter)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const isGlobalFilterEnabled = (options: DataGridOptions): boolean =>
|
|
81
|
+
resolveToolbar(options).search
|
|
82
|
+
|
|
83
|
+
const createActionButton = (icon: string, label: string, dataAttribute: string): HTMLButtonElement => {
|
|
84
|
+
const button = document.createElement('button')
|
|
85
|
+
button.type = 'button'
|
|
86
|
+
button.classList.add('btn', 'btn-ghost', 'btn-sm')
|
|
87
|
+
button.innerHTML = icon
|
|
88
|
+
button.setAttribute(dataAttribute, '')
|
|
89
|
+
button.setAttribute('data-coreui-tooltip', label)
|
|
90
|
+
const text = document.createElement('span')
|
|
91
|
+
text.classList.add('visually-hidden')
|
|
92
|
+
text.textContent = label
|
|
93
|
+
button.append(text)
|
|
94
|
+
return button
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const createToolbar = (context: GridToolbarContext): GridToolbar => {
|
|
98
|
+
const element = document.createElement('div')
|
|
99
|
+
element.classList.add(CLASS_NAME_TOOLBAR)
|
|
100
|
+
|
|
101
|
+
if (context.columns || context.export || context.history) {
|
|
102
|
+
const actions = document.createElement('div')
|
|
103
|
+
actions.classList.add(CLASS_NAME_ACTIONS)
|
|
104
|
+
|
|
105
|
+
if (context.columns) {
|
|
106
|
+
actions.append(
|
|
107
|
+
createActionButton(context.icons.columns, context.labels.toolbarColumns, 'data-coreui-toolbar-columns')
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (context.export) {
|
|
112
|
+
actions.append(
|
|
113
|
+
createActionButton(context.icons.export, context.labels.toolbarExport, 'data-coreui-toolbar-export')
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (context.history) {
|
|
118
|
+
const undoButton = createActionButton(context.icons.undo, context.labels.toolbarUndo, 'data-coreui-toolbar-undo')
|
|
119
|
+
const redoButton = createActionButton(context.icons.redo, context.labels.toolbarRedo, 'data-coreui-toolbar-redo')
|
|
120
|
+
undoButton.disabled = true
|
|
121
|
+
redoButton.disabled = true
|
|
122
|
+
actions.append(undoButton, redoButton)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
element.append(actions)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (context.search) {
|
|
129
|
+
const search = document.createElement('div')
|
|
130
|
+
search.classList.add(CLASS_NAME_SEARCH)
|
|
131
|
+
|
|
132
|
+
const input = document.createElement('input')
|
|
133
|
+
input.type = 'search'
|
|
134
|
+
input.classList.add('form-control', 'form-control-sm')
|
|
135
|
+
input.placeholder = context.labels.globalFilterPlaceholder
|
|
136
|
+
input.setAttribute('aria-label', context.labels.globalFilterLabel)
|
|
137
|
+
input.dataset.coreuiGlobalFilter = ''
|
|
138
|
+
|
|
139
|
+
search.append(input)
|
|
140
|
+
element.append(search)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { element }
|
|
144
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid ui/tooltip.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CLASS_NAME_TOOLTIP = 'data-grid-tooltip'
|
|
9
|
+
const ATTRIBUTE = 'data-coreui-tooltip'
|
|
10
|
+
const SELECTOR = `[${ATTRIBUTE}]`
|
|
11
|
+
const SHOW_DELAY = 200
|
|
12
|
+
const GAP = 6
|
|
13
|
+
const EDGE = 4
|
|
14
|
+
|
|
15
|
+
export interface GridTooltip {
|
|
16
|
+
dispose: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// A single shared, delegated tooltip for grid-owned icon controls. Positioned
|
|
20
|
+
// with position: fixed so it escapes the viewport's overflow, above the anchor
|
|
21
|
+
// with a flip below when there isn't room. Purely visual (aria-hidden) - the
|
|
22
|
+
// anchors already carry their accessible name via aria-label / hidden text.
|
|
23
|
+
export const createTooltip = (root: HTMLElement): GridTooltip => {
|
|
24
|
+
const element = document.createElement('div')
|
|
25
|
+
element.className = CLASS_NAME_TOOLTIP
|
|
26
|
+
element.setAttribute('role', 'tooltip')
|
|
27
|
+
element.setAttribute('aria-hidden', 'true')
|
|
28
|
+
element.hidden = true
|
|
29
|
+
root.append(element)
|
|
30
|
+
|
|
31
|
+
let anchor: HTMLElement | null = null
|
|
32
|
+
let showTimer: ReturnType<typeof setTimeout> | undefined
|
|
33
|
+
|
|
34
|
+
const anchorFrom = (target: EventTarget | null): HTMLElement | null => {
|
|
35
|
+
const element_ = target instanceof Element ? target.closest(SELECTOR) : null
|
|
36
|
+
return element_ instanceof HTMLElement && element_.getAttribute(ATTRIBUTE) ? element_ : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const position = (): void => {
|
|
40
|
+
if (!anchor) {
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rect = anchor.getBoundingClientRect()
|
|
45
|
+
const width = element.offsetWidth
|
|
46
|
+
const height = element.offsetHeight
|
|
47
|
+
const above = rect.top - height - GAP
|
|
48
|
+
const top = above >= 0 ? above : rect.bottom + GAP
|
|
49
|
+
const left = Math.max(EDGE, Math.min(rect.left + ((rect.width - width) / 2), window.innerWidth - width - EDGE))
|
|
50
|
+
element.style.top = `${top}px`
|
|
51
|
+
element.style.left = `${left}px`
|
|
52
|
+
element.style.visibility = 'visible'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const show = (target: HTMLElement): void => {
|
|
56
|
+
anchor = target
|
|
57
|
+
element.textContent = target.getAttribute(ATTRIBUTE)
|
|
58
|
+
element.style.visibility = 'hidden'
|
|
59
|
+
element.hidden = false
|
|
60
|
+
position()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const hide = (): void => {
|
|
64
|
+
clearTimeout(showTimer)
|
|
65
|
+
anchor = null
|
|
66
|
+
element.hidden = true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const onPointerOver = (event: PointerEvent): void => {
|
|
70
|
+
const target = anchorFrom(event.target)
|
|
71
|
+
if (!target || target === anchor) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
clearTimeout(showTimer)
|
|
76
|
+
showTimer = setTimeout(() => show(target), SHOW_DELAY)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const onPointerOut = (event: PointerEvent): void => {
|
|
80
|
+
if (anchor && event.relatedTarget instanceof Node && anchor.contains(event.relatedTarget)) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
hide()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const onFocusIn = (event: FocusEvent): void => {
|
|
88
|
+
const target = anchorFrom(event.target)
|
|
89
|
+
if (target) {
|
|
90
|
+
show(target)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
root.addEventListener('pointerover', onPointerOver as EventListener)
|
|
95
|
+
root.addEventListener('pointerout', onPointerOut as EventListener)
|
|
96
|
+
root.addEventListener('pointerdown', hide)
|
|
97
|
+
root.addEventListener('focusin', onFocusIn as EventListener)
|
|
98
|
+
root.addEventListener('focusout', hide)
|
|
99
|
+
window.addEventListener('scroll', hide, true)
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
dispose() {
|
|
103
|
+
clearTimeout(showTimer)
|
|
104
|
+
root.removeEventListener('pointerover', onPointerOver as EventListener)
|
|
105
|
+
root.removeEventListener('pointerout', onPointerOut as EventListener)
|
|
106
|
+
root.removeEventListener('pointerdown', hide)
|
|
107
|
+
root.removeEventListener('focusin', onFocusIn as EventListener)
|
|
108
|
+
root.removeEventListener('focusout', hide)
|
|
109
|
+
window.removeEventListener('scroll', hide, true)
|
|
110
|
+
element.remove()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Hand-written types for the vendored config.js (see the provenance header there)
|
|
2
|
+
export default class Config {
|
|
3
|
+
static get Default(): unknown
|
|
4
|
+
static get DefaultType(): unknown
|
|
5
|
+
static get NAME(): string
|
|
6
|
+
|
|
7
|
+
_getConfig(config?: unknown): any
|
|
8
|
+
_configAfterMerge(config: any): any
|
|
9
|
+
_mergeConfigObj(config?: unknown, element?: Element): any
|
|
10
|
+
_typeCheckConfig(config: Record<string, unknown>, configTypes?: Record<string, string>): void
|
|
11
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Vendored from https://github.com/coreui/coreui-pro/blob/bc4daf1cd79d527bf665d615c0a31f53ab0c3d4e/js/src/util/config.js — sync manually
|
|
2
|
+
/**
|
|
3
|
+
* --------------------------------------------------------------------------
|
|
4
|
+
* CoreUI util/config.js
|
|
5
|
+
* Licensed under MIT (https://github.com/coreui/coreui/blob/main/LICENSE)
|
|
6
|
+
*
|
|
7
|
+
* This is a modified version of the Bootstrap's util/config.js
|
|
8
|
+
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
|
9
|
+
* --------------------------------------------------------------------------
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import Manipulator from '../dom/manipulator.js'
|
|
13
|
+
import { isElement, toType } from './index.js'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Class definition
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
class Config {
|
|
20
|
+
// Getters
|
|
21
|
+
static get Default() {
|
|
22
|
+
return {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
static get DefaultType() {
|
|
26
|
+
return {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static get NAME() {
|
|
30
|
+
throw new Error('You have to implement the static method "NAME", for each component!')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_getConfig(config) {
|
|
34
|
+
config = this._mergeConfigObj(config)
|
|
35
|
+
config = this._configAfterMerge(config)
|
|
36
|
+
this._typeCheckConfig(config)
|
|
37
|
+
return config
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
_configAfterMerge(config) {
|
|
41
|
+
return config
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_mergeConfigObj(config, element) {
|
|
45
|
+
const jsonConfig = isElement(element) ? Manipulator.getDataAttribute(element, 'config') : {} // try to parse
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
...this.constructor.Default,
|
|
49
|
+
...(typeof jsonConfig === 'object' ? jsonConfig : {}),
|
|
50
|
+
...(isElement(element) ? Manipulator.getDataAttributes(element) : {}),
|
|
51
|
+
...(typeof config === 'object' ? config : {})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_typeCheckConfig(config, configTypes = this.constructor.DefaultType) {
|
|
56
|
+
for (const [property, expectedTypes] of Object.entries(configTypes)) {
|
|
57
|
+
const value = config[property]
|
|
58
|
+
const valueType = isElement(value) ? 'element' : toType(value)
|
|
59
|
+
|
|
60
|
+
if (!new RegExp(expectedTypes).test(valueType)) {
|
|
61
|
+
throw new TypeError(
|
|
62
|
+
`${this.constructor.NAME.toUpperCase()}: Option "${property}" provided type "${valueType}" but expected type "${expectedTypes}".`
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default Config
|
package/src/util/csv.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid util/csv.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Table } from '@tanstack/table-core'
|
|
9
|
+
import type { DataGridItem } from '../types'
|
|
10
|
+
|
|
11
|
+
export interface CsvOptions {
|
|
12
|
+
scope?: 'filtered' | 'all' | 'selected'
|
|
13
|
+
delimiter?: string
|
|
14
|
+
bom?: boolean
|
|
15
|
+
sanitize?: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CsvDownloadOptions extends CsvOptions {
|
|
19
|
+
filename?: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface CsvColumnMeta {
|
|
23
|
+
label?: string
|
|
24
|
+
formatter?: (value: unknown, item: DataGridItem) => string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const escapeField = (value: string, delimiter: string): string =>
|
|
28
|
+
/["\r\n]/.test(value) || value.includes(delimiter) ? `"${value.replace(/"/g, '""')}"` : value
|
|
29
|
+
|
|
30
|
+
// CSV formula injection: a leading =, +, -, @, tab or CR makes spreadsheets treat
|
|
31
|
+
// the cell as a formula. Opt-in guard prefixes such fields with an apostrophe.
|
|
32
|
+
const sanitizeField = (value: string): string =>
|
|
33
|
+
/^[=+\-@\t\r]/.test(value) ? `'${value}` : value
|
|
34
|
+
|
|
35
|
+
const rowsFor = (table: Table<DataGridItem>, scope: CsvOptions['scope']) => {
|
|
36
|
+
if (scope === 'all') {
|
|
37
|
+
return table.getCoreRowModel().rows
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (scope === 'selected') {
|
|
41
|
+
return table.getSelectedRowModel().rows
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return table.getSortedRowModel().rows
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const exportCsv = (table: Table<DataGridItem>, options: CsvOptions = {}): string => {
|
|
48
|
+
const delimiter = options.delimiter ?? ','
|
|
49
|
+
// Export follows the rendered layout: pinning, column order and visibility.
|
|
50
|
+
const columns = [
|
|
51
|
+
...table.getLeftVisibleLeafColumns(),
|
|
52
|
+
...table.getCenterVisibleLeafColumns(),
|
|
53
|
+
...table.getRightVisibleLeafColumns()
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
const process = (field: string) => escapeField(options.sanitize ? sanitizeField(field) : field, delimiter)
|
|
57
|
+
const line = (fields: string[]) => fields.map(process).join(delimiter)
|
|
58
|
+
|
|
59
|
+
const header = line(columns.map(column => String((column.columnDef.meta as CsvColumnMeta)?.label ?? column.id)))
|
|
60
|
+
const body = rowsFor(table, options.scope).map(row =>
|
|
61
|
+
line(columns.map(column => {
|
|
62
|
+
const value = row.getValue(column.id)
|
|
63
|
+
const meta = column.columnDef.meta as CsvColumnMeta
|
|
64
|
+
// Coerce: a formatter may return a number/null in plain-JS usage.
|
|
65
|
+
return meta?.formatter ? String(meta.formatter(value, row.original) ?? '') : String(value ?? '')
|
|
66
|
+
}))
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const csv = [header, ...body].join('\r\n')
|
|
70
|
+
return options.bom ? `\uFEFF${csv}` : csv
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const downloadCsv = (table: Table<DataGridItem>, options: CsvDownloadOptions = {}): void => {
|
|
74
|
+
const blob = new Blob([exportCsv(table, options)], { type: 'text/csv;charset=utf-8;' })
|
|
75
|
+
const url = URL.createObjectURL(blob)
|
|
76
|
+
const anchor = document.createElement('a')
|
|
77
|
+
anchor.href = url
|
|
78
|
+
anchor.download = options.filename ?? 'export.csv'
|
|
79
|
+
anchor.style.display = 'none'
|
|
80
|
+
// Attach before clicking (older Firefox) and defer revoke (Safari race).
|
|
81
|
+
document.body.append(anchor)
|
|
82
|
+
anchor.click()
|
|
83
|
+
anchor.remove()
|
|
84
|
+
setTimeout(() => URL.revokeObjectURL(url))
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------------------------------------
|
|
3
|
+
* CoreUI PRO data-grid util/debounce.ts
|
|
4
|
+
* License (https://coreui.io/pro/license/)
|
|
5
|
+
* --------------------------------------------------------------------------
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const debounce = <Args extends unknown[]>(
|
|
9
|
+
callback: (...args: Args) => void,
|
|
10
|
+
delay: number
|
|
11
|
+
): ((...args: Args) => void) => {
|
|
12
|
+
let timeout: ReturnType<typeof setTimeout> | undefined
|
|
13
|
+
|
|
14
|
+
return (...args: Args) => {
|
|
15
|
+
clearTimeout(timeout)
|
|
16
|
+
timeout = setTimeout(() => callback(...args), delay)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Hand-written types for the vendored util/index.js (see the provenance header there)
|
|
2
|
+
export function defineJQueryPlugin(plugin: { NAME: string; jQueryInterface: (this: any, ...args: any[]) => unknown }): void
|
|
3
|
+
export function executeAfterTransition(callback: () => void, transitionElement: Element, waitForTransition?: boolean): void
|
|
4
|
+
export function getElement(object: unknown): HTMLElement | null
|
|
5
|
+
export function getjQuery(): unknown
|
|
6
|
+
export function getUID(prefix: string): string
|
|
7
|
+
export function isElement(object: unknown): boolean
|
|
8
|
+
export function isRTL(): boolean
|
|
9
|
+
export function isVisible(element: Element): boolean
|
|
10
|
+
export function toType(object: unknown): string
|