@tipolox/litgrid-core 1.0.3 → 1.1.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/README.md +1 -0
- package/package.json +6 -2
- package/src/core/GridEngine.ts +38 -0
- package/src/core/createGridEngine.ts +217 -0
- package/src/features/filtering/filtering.ts +63 -0
- package/src/features/pagination/pagination.ts +35 -0
- package/src/features/quick-search/quickSearch.ts +31 -0
- package/src/features/selection/selection.ts +111 -0
- package/src/features/sorting/sorting.ts +46 -0
- package/src/features/transforms/applyDataTransforms.ts +13 -0
- package/src/index.ts +11 -0
- package/src/model/GridConfig.ts +61 -0
- package/src/model/GridState.ts +57 -0
- package/src/utils/range.ts +11 -0
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ npm install @tipolox/litgrid-core
|
|
|
13
13
|
- [Core engine API](https://github.com/tipolox/litgrid/blob/main/docs/api-reference.md#core-engine)
|
|
14
14
|
- [Installation guide](https://github.com/tipolox/litgrid/blob/main/docs/installation.md)
|
|
15
15
|
- [Repository](https://github.com/tipolox/litgrid)
|
|
16
|
+
- [v1.1.0 upgrade guide](https://github.com/tipolox/litgrid/blob/main/docs/installation.md#upgrading-to-v110)
|
|
16
17
|
- [Live demo](https://tipolox.com/litgrid/demo)
|
|
17
18
|
|
|
18
19
|
Licensed under the [MIT License](https://github.com/tipolox/litgrid/blob/main/LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tipolox/litgrid-core",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Framework-agnostic data engine for LitGrid.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
"./package.json": "./package.json"
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
18
|
-
"dist"
|
|
18
|
+
"dist",
|
|
19
|
+
"src/**/*.ts",
|
|
20
|
+
"!src/**/*.d.ts",
|
|
21
|
+
"!src/**/*.test.ts",
|
|
22
|
+
"!src/**/*.bench.ts"
|
|
19
23
|
],
|
|
20
24
|
"keywords": [
|
|
21
25
|
"data-grid",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { GridConfig, ResolvedGridConfig } from '../model/GridConfig.js'
|
|
2
|
+
import type {
|
|
3
|
+
GridSelection,
|
|
4
|
+
GridFilter,
|
|
5
|
+
GridPaginationState,
|
|
6
|
+
GridSortState,
|
|
7
|
+
SelectionIntent,
|
|
8
|
+
SortDirection
|
|
9
|
+
} from '../model/GridState.js'
|
|
10
|
+
|
|
11
|
+
export type GridEngine = {
|
|
12
|
+
setConfig: (config: GridConfig) => void
|
|
13
|
+
getConfig: () => ResolvedGridConfig
|
|
14
|
+
setData: (data: unknown[]) => void
|
|
15
|
+
getRows: () => unknown[]
|
|
16
|
+
setQuickSearch: (query: string) => void
|
|
17
|
+
clearQuickSearch: () => void
|
|
18
|
+
getQuickSearch: () => string
|
|
19
|
+
setFilter: (filter: GridFilter) => void
|
|
20
|
+
clearFilter: (columnKey?: string) => void
|
|
21
|
+
getFilters: () => GridFilter[]
|
|
22
|
+
setPage: (pageIndex: number) => void
|
|
23
|
+
setPageSize: (pageSize: number) => void
|
|
24
|
+
getPagination: () => GridPaginationState
|
|
25
|
+
getTotalRowCount: () => number
|
|
26
|
+
sortBy: (columnKey: string, direction: SortDirection) => void
|
|
27
|
+
clearSort: () => void
|
|
28
|
+
getSort: () => GridSortState
|
|
29
|
+
getRowCount: () => number
|
|
30
|
+
getVisibleRows: (startIndex: number, endIndex: number) => unknown[]
|
|
31
|
+
selectRow: (rowIndex: number, intent?: SelectionIntent) => void
|
|
32
|
+
selectAllRows: () => void
|
|
33
|
+
selectCell: (rowIndex: number, columnKey: string) => void
|
|
34
|
+
clearSelection: () => void
|
|
35
|
+
getSelection: () => GridSelection
|
|
36
|
+
isRowSelected: (rowIndex: number) => boolean
|
|
37
|
+
isCellSelected: (rowIndex: number, columnKey: string) => boolean
|
|
38
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import type { GridConfig } from '../model/GridConfig.js'
|
|
2
|
+
import { normalizeConfig } from '../model/GridConfig.js'
|
|
3
|
+
import type { GridFilter, GridState, SelectionIntent, SortDirection } from '../model/GridState.js'
|
|
4
|
+
import {
|
|
5
|
+
createEmptySelection,
|
|
6
|
+
getCellKey,
|
|
7
|
+
getNextCellSelection,
|
|
8
|
+
getNextRowSelection
|
|
9
|
+
} from '../features/selection/selection.js'
|
|
10
|
+
import { applyDataTransforms } from '../features/transforms/applyDataTransforms.js'
|
|
11
|
+
import { clampPageIndex, createPaginationState, paginateData } from '../features/pagination/pagination.js'
|
|
12
|
+
import type { GridEngine } from './GridEngine.js'
|
|
13
|
+
|
|
14
|
+
export function createGridEngine(config: GridConfig = {}): GridEngine {
|
|
15
|
+
const initialConfig = normalizeConfig(config)
|
|
16
|
+
let state: GridState = {
|
|
17
|
+
config: initialConfig,
|
|
18
|
+
originalData: [],
|
|
19
|
+
transformedData: [],
|
|
20
|
+
data: [],
|
|
21
|
+
sort: { columnKey: null, direction: null },
|
|
22
|
+
quickSearchQuery: '',
|
|
23
|
+
filters: [],
|
|
24
|
+
pagination: createPaginationState(
|
|
25
|
+
initialConfig.pagination.enabled,
|
|
26
|
+
initialConfig.pagination.pageSize,
|
|
27
|
+
initialConfig.pagination.pageIndex
|
|
28
|
+
),
|
|
29
|
+
selection: createEmptySelection(initialConfig.selection.mode)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function refreshData() {
|
|
33
|
+
state.transformedData = applyDataTransforms(
|
|
34
|
+
state.originalData,
|
|
35
|
+
state.quickSearchQuery,
|
|
36
|
+
state.filters,
|
|
37
|
+
state.sort
|
|
38
|
+
)
|
|
39
|
+
const pageCount = Math.ceil(state.transformedData.length / state.pagination.pageSize)
|
|
40
|
+
state.pagination = {
|
|
41
|
+
...state.pagination,
|
|
42
|
+
totalRows: state.transformedData.length,
|
|
43
|
+
pageCount,
|
|
44
|
+
pageIndex: clampPageIndex(state.pagination.pageIndex, pageCount)
|
|
45
|
+
}
|
|
46
|
+
state.data = paginateData(state.transformedData, state.pagination)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resetPage() {
|
|
50
|
+
state.pagination = { ...state.pagination, pageIndex: 0 }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
setConfig(config: GridConfig) {
|
|
55
|
+
const nextConfig = normalizeConfig(config)
|
|
56
|
+
|
|
57
|
+
state.config = nextConfig
|
|
58
|
+
state.pagination = createPaginationState(
|
|
59
|
+
nextConfig.pagination.enabled,
|
|
60
|
+
nextConfig.pagination.pageSize,
|
|
61
|
+
nextConfig.pagination.pageIndex,
|
|
62
|
+
state.transformedData.length
|
|
63
|
+
)
|
|
64
|
+
refreshData()
|
|
65
|
+
|
|
66
|
+
if (state.selection.mode !== nextConfig.selection.mode) {
|
|
67
|
+
state.selection = createEmptySelection(nextConfig.selection.mode)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
getConfig() {
|
|
71
|
+
return state.config
|
|
72
|
+
},
|
|
73
|
+
setData(data: unknown[]) {
|
|
74
|
+
state.originalData = data ?? []
|
|
75
|
+
resetPage()
|
|
76
|
+
refreshData()
|
|
77
|
+
},
|
|
78
|
+
getRows() {
|
|
79
|
+
return state.data
|
|
80
|
+
},
|
|
81
|
+
setQuickSearch(query: string) {
|
|
82
|
+
state.quickSearchQuery = typeof query === 'string' ? query : ''
|
|
83
|
+
resetPage()
|
|
84
|
+
refreshData()
|
|
85
|
+
},
|
|
86
|
+
clearQuickSearch() {
|
|
87
|
+
state.quickSearchQuery = ''
|
|
88
|
+
resetPage()
|
|
89
|
+
refreshData()
|
|
90
|
+
},
|
|
91
|
+
getQuickSearch() {
|
|
92
|
+
return state.quickSearchQuery
|
|
93
|
+
},
|
|
94
|
+
setFilter(filter: GridFilter) {
|
|
95
|
+
state.filters = [
|
|
96
|
+
...state.filters.filter((item) => item.columnKey !== filter.columnKey),
|
|
97
|
+
{ ...filter }
|
|
98
|
+
]
|
|
99
|
+
resetPage()
|
|
100
|
+
refreshData()
|
|
101
|
+
},
|
|
102
|
+
clearFilter(columnKey?: string) {
|
|
103
|
+
state.filters = columnKey
|
|
104
|
+
? state.filters.filter((item) => item.columnKey !== columnKey)
|
|
105
|
+
: []
|
|
106
|
+
resetPage()
|
|
107
|
+
refreshData()
|
|
108
|
+
},
|
|
109
|
+
getFilters() {
|
|
110
|
+
return state.filters.slice()
|
|
111
|
+
},
|
|
112
|
+
setPage(pageIndex: number) {
|
|
113
|
+
const nextPageIndex = Number.isFinite(pageIndex) ? Math.max(0, Math.floor(pageIndex)) : 0
|
|
114
|
+
state.pagination = {
|
|
115
|
+
...state.pagination,
|
|
116
|
+
pageIndex: clampPageIndex(nextPageIndex, state.pagination.pageCount)
|
|
117
|
+
}
|
|
118
|
+
state.data = paginateData(state.transformedData, state.pagination)
|
|
119
|
+
},
|
|
120
|
+
setPageSize(pageSize: number) {
|
|
121
|
+
if (!Number.isFinite(pageSize) || pageSize <= 0) {
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
state.pagination = {
|
|
126
|
+
...state.pagination,
|
|
127
|
+
pageSize: Math.max(1, Math.floor(pageSize)),
|
|
128
|
+
pageIndex: 0
|
|
129
|
+
}
|
|
130
|
+
refreshData()
|
|
131
|
+
},
|
|
132
|
+
getPagination() {
|
|
133
|
+
return { ...state.pagination }
|
|
134
|
+
},
|
|
135
|
+
getTotalRowCount() {
|
|
136
|
+
return state.pagination.totalRows
|
|
137
|
+
},
|
|
138
|
+
sortBy(columnKey: string, direction: SortDirection) {
|
|
139
|
+
state.sort = { columnKey, direction }
|
|
140
|
+
resetPage()
|
|
141
|
+
refreshData()
|
|
142
|
+
},
|
|
143
|
+
clearSort() {
|
|
144
|
+
state.sort = { columnKey: null, direction: null }
|
|
145
|
+
resetPage()
|
|
146
|
+
refreshData()
|
|
147
|
+
},
|
|
148
|
+
getSort() {
|
|
149
|
+
return state.sort
|
|
150
|
+
},
|
|
151
|
+
getRowCount() {
|
|
152
|
+
return state.data.length
|
|
153
|
+
},
|
|
154
|
+
getVisibleRows(startIndex: number, endIndex: number) {
|
|
155
|
+
return state.data.slice(startIndex, endIndex)
|
|
156
|
+
},
|
|
157
|
+
selectRow(rowIndex: number, intent: SelectionIntent = 'replace') {
|
|
158
|
+
const nextSelection = getNextRowSelection(
|
|
159
|
+
state.selection,
|
|
160
|
+
state.config.selection.mode,
|
|
161
|
+
rowIndex,
|
|
162
|
+
intent
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (nextSelection) {
|
|
166
|
+
state.selection = nextSelection
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
selectAllRows() {
|
|
170
|
+
if (state.config.selection.mode !== 'multi-row') {
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const rowIndexes = new Set<number>()
|
|
175
|
+
for (let rowIndex = 0; rowIndex < state.data.length; rowIndex += 1) {
|
|
176
|
+
rowIndexes.add(rowIndex)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
state.selection = {
|
|
180
|
+
...createEmptySelection('multi-row'),
|
|
181
|
+
rowIndex: state.data.length > 0 ? state.data.length - 1 : null,
|
|
182
|
+
anchorRowIndex: state.data.length > 0 ? 0 : null,
|
|
183
|
+
rowIndexes
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
selectCell(rowIndex: number, columnKey: string) {
|
|
187
|
+
const nextSelection = getNextCellSelection(
|
|
188
|
+
state.selection,
|
|
189
|
+
state.config.selection.mode,
|
|
190
|
+
rowIndex,
|
|
191
|
+
columnKey
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
if (nextSelection) {
|
|
195
|
+
state.selection = nextSelection
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
clearSelection() {
|
|
199
|
+
state.selection = createEmptySelection(state.config.selection.mode)
|
|
200
|
+
},
|
|
201
|
+
getSelection() {
|
|
202
|
+
return state.selection
|
|
203
|
+
},
|
|
204
|
+
isRowSelected(rowIndex: number) {
|
|
205
|
+
return (
|
|
206
|
+
(state.selection.mode === 'row' || state.selection.mode === 'multi-row') &&
|
|
207
|
+
state.selection.rowIndexes.has(rowIndex)
|
|
208
|
+
)
|
|
209
|
+
},
|
|
210
|
+
isCellSelected(rowIndex: number, columnKey: string) {
|
|
211
|
+
return (
|
|
212
|
+
(state.selection.mode === 'cell' || state.selection.mode === 'multi-cell') &&
|
|
213
|
+
state.selection.cells.has(getCellKey(rowIndex, columnKey))
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { GridFilter } from '../../model/GridState.js'
|
|
2
|
+
|
|
3
|
+
function getRowFilterValue(row: unknown, columnKey: string) {
|
|
4
|
+
if (row && typeof row === 'object' && !Array.isArray(row) && columnKey in row) {
|
|
5
|
+
return (row as Record<string, unknown>)[columnKey]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return row
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isEmpty(value: unknown) {
|
|
12
|
+
return value === null || value === undefined || value === ''
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeText(value: unknown) {
|
|
16
|
+
return String(value ?? '').toLocaleLowerCase()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function compareValues(left: unknown, right: unknown) {
|
|
20
|
+
if (typeof left === 'number') {
|
|
21
|
+
const numericRight = typeof right === 'number' ? right : Number(right)
|
|
22
|
+
if (!Number.isNaN(numericRight)) {
|
|
23
|
+
return left - numericRight
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return normalizeText(left).localeCompare(normalizeText(right))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function matchesFilter(value: unknown, filter: GridFilter) {
|
|
31
|
+
switch (filter.operator) {
|
|
32
|
+
case 'isEmpty':
|
|
33
|
+
return isEmpty(value)
|
|
34
|
+
case 'isNotEmpty':
|
|
35
|
+
return !isEmpty(value)
|
|
36
|
+
case 'contains':
|
|
37
|
+
return normalizeText(value).includes(normalizeText(filter.value))
|
|
38
|
+
case 'equals':
|
|
39
|
+
return compareValues(value, filter.value) === 0
|
|
40
|
+
case 'startsWith':
|
|
41
|
+
return normalizeText(value).startsWith(normalizeText(filter.value))
|
|
42
|
+
case 'endsWith':
|
|
43
|
+
return normalizeText(value).endsWith(normalizeText(filter.value))
|
|
44
|
+
case 'greaterThan':
|
|
45
|
+
return compareValues(value, filter.value) > 0
|
|
46
|
+
case 'greaterThanOrEqual':
|
|
47
|
+
return compareValues(value, filter.value) >= 0
|
|
48
|
+
case 'lessThan':
|
|
49
|
+
return compareValues(value, filter.value) < 0
|
|
50
|
+
case 'lessThanOrEqual':
|
|
51
|
+
return compareValues(value, filter.value) <= 0
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function applyFilterData(data: unknown[], filters: GridFilter[]) {
|
|
56
|
+
if (filters.length === 0) {
|
|
57
|
+
return data.slice()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return data.filter((row) =>
|
|
61
|
+
filters.every((filter) => matchesFilter(getRowFilterValue(row, filter.columnKey), filter))
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { GridPaginationState } from '../../model/GridState.js'
|
|
2
|
+
|
|
3
|
+
export function getPageCount(totalRows: number, pageSize: number) {
|
|
4
|
+
return totalRows === 0 ? 0 : Math.ceil(totalRows / pageSize)
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function clampPageIndex(pageIndex: number, pageCount: number) {
|
|
8
|
+
return pageCount === 0 ? 0 : Math.min(Math.max(0, pageIndex), pageCount - 1)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function createPaginationState(
|
|
12
|
+
enabled: boolean,
|
|
13
|
+
pageSize: number,
|
|
14
|
+
pageIndex = 0,
|
|
15
|
+
totalRows = 0
|
|
16
|
+
): GridPaginationState {
|
|
17
|
+
const pageCount = getPageCount(totalRows, pageSize)
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
enabled,
|
|
21
|
+
pageSize,
|
|
22
|
+
pageIndex: clampPageIndex(pageIndex, pageCount),
|
|
23
|
+
totalRows,
|
|
24
|
+
pageCount
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function paginateData(data: unknown[], pagination: GridPaginationState) {
|
|
29
|
+
if (!pagination.enabled) {
|
|
30
|
+
return data
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const start = pagination.pageIndex * pagination.pageSize
|
|
34
|
+
return data.slice(start, start + pagination.pageSize)
|
|
35
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
function normalizeQuery(query: string) {
|
|
2
|
+
return query.trim().toLocaleLowerCase()
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function matchesValue(value: unknown, query: string) {
|
|
6
|
+
return (
|
|
7
|
+
(typeof value === 'string' ||
|
|
8
|
+
typeof value === 'number' ||
|
|
9
|
+
typeof value === 'boolean' ||
|
|
10
|
+
typeof value === 'bigint') &&
|
|
11
|
+
String(value).toLocaleLowerCase().includes(query)
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function matchesRow(row: unknown, query: string) {
|
|
16
|
+
if (row && typeof row === 'object' && !Array.isArray(row)) {
|
|
17
|
+
return Object.values(row as Record<string, unknown>).some((value) => matchesValue(value, query))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return matchesValue(row, query)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function applyQuickSearch(data: unknown[], query: string) {
|
|
24
|
+
const normalizedQuery = normalizeQuery(query)
|
|
25
|
+
|
|
26
|
+
if (!normalizedQuery) {
|
|
27
|
+
return data.slice()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return data.filter((row) => matchesRow(row, normalizedQuery))
|
|
31
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { SelectionMode } from '../../model/GridConfig.js'
|
|
2
|
+
import type { GridSelection, SelectionIntent } from '../../model/GridState.js'
|
|
3
|
+
import { getRowRange } from '../../utils/range.js'
|
|
4
|
+
|
|
5
|
+
export function createEmptySelection(mode: SelectionMode): GridSelection {
|
|
6
|
+
return {
|
|
7
|
+
mode,
|
|
8
|
+
rowIndex: null,
|
|
9
|
+
columnKey: null,
|
|
10
|
+
anchorRowIndex: null,
|
|
11
|
+
rowIndexes: new Set(),
|
|
12
|
+
cells: new Set()
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function getCellKey(rowIndex: number, columnKey: string) {
|
|
17
|
+
return `${rowIndex}:${columnKey}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getNextRowSelection(
|
|
21
|
+
selection: GridSelection,
|
|
22
|
+
mode: SelectionMode,
|
|
23
|
+
rowIndex: number,
|
|
24
|
+
intent: SelectionIntent
|
|
25
|
+
): GridSelection | null {
|
|
26
|
+
if (mode === 'row') {
|
|
27
|
+
return {
|
|
28
|
+
...createEmptySelection('row'),
|
|
29
|
+
rowIndex,
|
|
30
|
+
anchorRowIndex: rowIndex,
|
|
31
|
+
rowIndexes: new Set([rowIndex])
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (mode !== 'multi-row') {
|
|
36
|
+
return null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (intent === 'range') {
|
|
40
|
+
const anchorRowIndex = selection.anchorRowIndex ?? rowIndex
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...createEmptySelection('multi-row'),
|
|
44
|
+
rowIndex,
|
|
45
|
+
anchorRowIndex,
|
|
46
|
+
rowIndexes: getRowRange(anchorRowIndex, rowIndex)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (intent === 'toggle') {
|
|
51
|
+
const rowIndexes = new Set(selection.rowIndexes)
|
|
52
|
+
|
|
53
|
+
if (rowIndexes.has(rowIndex)) {
|
|
54
|
+
rowIndexes.delete(rowIndex)
|
|
55
|
+
} else {
|
|
56
|
+
rowIndexes.add(rowIndex)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
...createEmptySelection('multi-row'),
|
|
61
|
+
rowIndex: rowIndexes.has(rowIndex) ? rowIndex : null,
|
|
62
|
+
anchorRowIndex: rowIndex,
|
|
63
|
+
rowIndexes
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
...createEmptySelection('multi-row'),
|
|
69
|
+
rowIndex,
|
|
70
|
+
anchorRowIndex: rowIndex,
|
|
71
|
+
rowIndexes: new Set([rowIndex])
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getNextCellSelection(
|
|
76
|
+
selection: GridSelection,
|
|
77
|
+
mode: SelectionMode,
|
|
78
|
+
rowIndex: number,
|
|
79
|
+
columnKey: string
|
|
80
|
+
): GridSelection | null {
|
|
81
|
+
if (mode === 'cell') {
|
|
82
|
+
return {
|
|
83
|
+
...createEmptySelection('cell'),
|
|
84
|
+
rowIndex,
|
|
85
|
+
columnKey,
|
|
86
|
+
cells: new Set([getCellKey(rowIndex, columnKey)])
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (mode !== 'multi-cell') {
|
|
91
|
+
return null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const cellKey = getCellKey(rowIndex, columnKey)
|
|
95
|
+
const cells = new Set(selection.cells)
|
|
96
|
+
|
|
97
|
+
if (cells.has(cellKey)) {
|
|
98
|
+
cells.delete(cellKey)
|
|
99
|
+
} else {
|
|
100
|
+
cells.add(cellKey)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...createEmptySelection('multi-cell'),
|
|
105
|
+
rowIndex: cells.has(cellKey) ? rowIndex : null,
|
|
106
|
+
columnKey: cells.has(cellKey) ? columnKey : null,
|
|
107
|
+
cells
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export { getRowRange }
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { GridSortState } from '../../model/GridState.js'
|
|
2
|
+
|
|
3
|
+
function getRowSortValue(row: unknown, columnKey: string) {
|
|
4
|
+
if (row && typeof row === 'object' && !Array.isArray(row) && columnKey in row) {
|
|
5
|
+
return (row as Record<string, unknown>)[columnKey]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
return row
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function normalizeSortValue(value: unknown) {
|
|
12
|
+
if (value === null || value === undefined) {
|
|
13
|
+
return ''
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof value === 'number') {
|
|
17
|
+
return value
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return String(value).toLowerCase()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function compareValues(left: unknown, right: unknown) {
|
|
24
|
+
const normalizedLeft = normalizeSortValue(left)
|
|
25
|
+
const normalizedRight = normalizeSortValue(right)
|
|
26
|
+
|
|
27
|
+
if (typeof normalizedLeft === 'number' && typeof normalizedRight === 'number') {
|
|
28
|
+
return normalizedLeft - normalizedRight
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return String(normalizedLeft).localeCompare(String(normalizedRight))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function applySortData(data: unknown[], sort: GridSortState) {
|
|
35
|
+
if (!sort.columnKey || !sort.direction) {
|
|
36
|
+
return data.slice()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return data.slice().sort((a, b) => {
|
|
40
|
+
const left = getRowSortValue(a, sort.columnKey!)
|
|
41
|
+
const right = getRowSortValue(b, sort.columnKey!)
|
|
42
|
+
const comparison = compareValues(left, right)
|
|
43
|
+
|
|
44
|
+
return sort.direction === 'asc' ? comparison : -comparison
|
|
45
|
+
})
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { GridFilter, GridSortState } from '../../model/GridState.js'
|
|
2
|
+
import { applyFilterData } from '../filtering/filtering.js'
|
|
3
|
+
import { applyQuickSearch } from '../quick-search/quickSearch.js'
|
|
4
|
+
import { applySortData } from '../sorting/sorting.js'
|
|
5
|
+
|
|
6
|
+
export function applyDataTransforms(
|
|
7
|
+
data: unknown[],
|
|
8
|
+
quickSearchQuery: string,
|
|
9
|
+
filters: GridFilter[],
|
|
10
|
+
sort: GridSortState
|
|
11
|
+
) {
|
|
12
|
+
return applySortData(applyFilterData(applyQuickSearch(data, quickSearchQuery), filters), sort)
|
|
13
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { createGridEngine } from './core/createGridEngine.js'
|
|
2
|
+
export type { GridEngine } from './core/GridEngine.js'
|
|
3
|
+
export type { GridConfig, GridPaginationConfig, ResolvedGridConfig, SelectionMode } from './model/GridConfig.js'
|
|
4
|
+
export type {
|
|
5
|
+
FilterOperator,
|
|
6
|
+
GridFilter,
|
|
7
|
+
GridPaginationState,
|
|
8
|
+
GridSelection,
|
|
9
|
+
SelectionIntent,
|
|
10
|
+
SortDirection
|
|
11
|
+
} from './model/GridState.js'
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type SelectionMode = 'none' | 'row' | 'multi-row' | 'cell' | 'multi-cell'
|
|
2
|
+
|
|
3
|
+
export type GridPaginationConfig = {
|
|
4
|
+
enabled?: boolean
|
|
5
|
+
pageSize?: number
|
|
6
|
+
pageIndex?: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type GridConfig = {
|
|
10
|
+
selection?: {
|
|
11
|
+
mode?: SelectionMode
|
|
12
|
+
checkboxes?: boolean
|
|
13
|
+
}
|
|
14
|
+
rowHeader?: {
|
|
15
|
+
enabled?: boolean
|
|
16
|
+
width?: number
|
|
17
|
+
}
|
|
18
|
+
pagination?: GridPaginationConfig
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ResolvedGridConfig = {
|
|
22
|
+
selection: {
|
|
23
|
+
mode: SelectionMode
|
|
24
|
+
checkboxes: boolean
|
|
25
|
+
}
|
|
26
|
+
rowHeader: {
|
|
27
|
+
enabled: boolean
|
|
28
|
+
width: number
|
|
29
|
+
}
|
|
30
|
+
pagination: {
|
|
31
|
+
enabled: boolean
|
|
32
|
+
pageSize: number
|
|
33
|
+
pageIndex: number
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePageSize(value: number | undefined) {
|
|
38
|
+
return Number.isFinite(value) && value! > 0 ? Math.max(1, Math.floor(value!)) : 50
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizePageIndex(value: number | undefined) {
|
|
42
|
+
return Number.isFinite(value) && value! >= 0 ? Math.floor(value!) : 0
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function normalizeConfig(config: GridConfig = {}): ResolvedGridConfig {
|
|
46
|
+
return {
|
|
47
|
+
selection: {
|
|
48
|
+
mode: config.selection?.mode ?? 'none',
|
|
49
|
+
checkboxes: config.selection?.checkboxes ?? false
|
|
50
|
+
},
|
|
51
|
+
rowHeader: {
|
|
52
|
+
enabled: config.rowHeader?.enabled ?? false,
|
|
53
|
+
width: config.rowHeader?.width ?? 56
|
|
54
|
+
},
|
|
55
|
+
pagination: {
|
|
56
|
+
enabled: config.pagination?.enabled ?? false,
|
|
57
|
+
pageSize: normalizePageSize(config.pagination?.pageSize),
|
|
58
|
+
pageIndex: normalizePageIndex(config.pagination?.pageIndex)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ResolvedGridConfig, SelectionMode } from './GridConfig.js'
|
|
2
|
+
|
|
3
|
+
export type SelectionIntent = 'replace' | 'toggle' | 'range'
|
|
4
|
+
|
|
5
|
+
export type GridSelection = {
|
|
6
|
+
mode: SelectionMode
|
|
7
|
+
rowIndex: number | null
|
|
8
|
+
columnKey: string | null
|
|
9
|
+
anchorRowIndex: number | null
|
|
10
|
+
rowIndexes: Set<number>
|
|
11
|
+
cells: Set<string>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SortDirection = 'asc' | 'desc'
|
|
15
|
+
|
|
16
|
+
export type GridSortState = {
|
|
17
|
+
columnKey: string | null
|
|
18
|
+
direction: SortDirection | null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type FilterOperator =
|
|
22
|
+
| 'contains'
|
|
23
|
+
| 'equals'
|
|
24
|
+
| 'startsWith'
|
|
25
|
+
| 'endsWith'
|
|
26
|
+
| 'isEmpty'
|
|
27
|
+
| 'isNotEmpty'
|
|
28
|
+
| 'greaterThan'
|
|
29
|
+
| 'greaterThanOrEqual'
|
|
30
|
+
| 'lessThan'
|
|
31
|
+
| 'lessThanOrEqual'
|
|
32
|
+
|
|
33
|
+
export type GridFilter = {
|
|
34
|
+
columnKey: string
|
|
35
|
+
operator: FilterOperator
|
|
36
|
+
value?: unknown
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type GridPaginationState = {
|
|
40
|
+
enabled: boolean
|
|
41
|
+
pageIndex: number
|
|
42
|
+
pageSize: number
|
|
43
|
+
totalRows: number
|
|
44
|
+
pageCount: number
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type GridState = {
|
|
48
|
+
config: ResolvedGridConfig
|
|
49
|
+
originalData: unknown[]
|
|
50
|
+
transformedData: unknown[]
|
|
51
|
+
data: unknown[]
|
|
52
|
+
sort: GridSortState
|
|
53
|
+
quickSearchQuery: string
|
|
54
|
+
filters: GridFilter[]
|
|
55
|
+
pagination: GridPaginationState
|
|
56
|
+
selection: GridSelection
|
|
57
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function getRowRange(startIndex: number, endIndex: number) {
|
|
2
|
+
const firstIndex = Math.min(startIndex, endIndex)
|
|
3
|
+
const lastIndex = Math.max(startIndex, endIndex)
|
|
4
|
+
const rowIndexes = new Set<number>()
|
|
5
|
+
|
|
6
|
+
for (let index = firstIndex; index <= lastIndex; index += 1) {
|
|
7
|
+
rowIndexes.add(index)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return rowIndexes
|
|
11
|
+
}
|