@postxl/generators 1.1.1 → 1.2.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/dist/frontend-core/frontend.generator.d.ts +0 -58
- package/dist/frontend-core/frontend.generator.js +6 -172
- package/dist/frontend-core/frontend.generator.js.map +1 -1
- package/dist/frontend-core/template/README.md +1 -1
- package/dist/frontend-core/template/src/components/admin/table-filter.tsx +1 -5
- package/dist/frontend-core/template/src/components/ui/color-mode-toggle/color-mode-toggle.tsx +10 -4
- package/dist/frontend-core/template/src/pages/dashboard/dashboard.page.tsx +2 -3
- package/dist/frontend-core/template/src/pages/error/default-error.page.tsx +1 -1
- package/dist/frontend-core/template/src/pages/error/not-found-error.page.tsx +1 -1
- package/dist/frontend-core/template/src/styles/styles.css +13 -1
- package/dist/frontend-core/template/tsconfig.json +2 -0
- package/dist/frontend-core/types/component.d.ts +1 -1
- package/dist/frontend-forms/generators/discriminated-union/fields.generator.js +4 -6
- package/dist/frontend-forms/generators/discriminated-union/fields.generator.js.map +1 -1
- package/dist/frontend-forms/generators/discriminated-union/inputs.generator.js +1 -1
- package/dist/frontend-forms/generators/discriminated-union/inputs.generator.js.map +1 -1
- package/dist/frontend-forms/generators/enum/inputs.generator.js +1 -1
- package/dist/frontend-forms/generators/enum/inputs.generator.js.map +1 -1
- package/dist/frontend-forms/generators/model/forms.generator.js +8 -12
- package/dist/frontend-forms/generators/model/forms.generator.js.map +1 -1
- package/dist/frontend-forms/generators/model/inputs.generator.js +2 -6
- package/dist/frontend-forms/generators/model/inputs.generator.js.map +1 -1
- package/dist/frontend-forms/template/src/components/ui/field/field.tsx +1 -4
- package/dist/frontend-tables/generators/model-table.generator.js +1 -5
- package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
- package/package.json +3 -2
- package/dist/frontend-core/template/src/components/ui/accordion/accordion.stories.tsx +0 -47
- package/dist/frontend-core/template/src/components/ui/accordion/accordion.tsx +0 -52
- package/dist/frontend-core/template/src/components/ui/admin-sidebar/admin-sidebar.tsx +0 -195
- package/dist/frontend-core/template/src/components/ui/alert/alert.stories.tsx +0 -61
- package/dist/frontend-core/template/src/components/ui/alert/alert.tsx +0 -45
- package/dist/frontend-core/template/src/components/ui/alert-dialog/alert-dialog.stories.tsx +0 -52
- package/dist/frontend-core/template/src/components/ui/alert-dialog/alert-dialog.tsx +0 -105
- package/dist/frontend-core/template/src/components/ui/avatar/avatar.stories.tsx +0 -30
- package/dist/frontend-core/template/src/components/ui/avatar/avatar.tsx +0 -39
- package/dist/frontend-core/template/src/components/ui/badge/badge.stories.tsx +0 -78
- package/dist/frontend-core/template/src/components/ui/badge/badge.tsx +0 -48
- package/dist/frontend-core/template/src/components/ui/breadcrumb/breadcrumb.stories.tsx +0 -67
- package/dist/frontend-core/template/src/components/ui/breadcrumb/breadcrumb.tsx +0 -85
- package/dist/frontend-core/template/src/components/ui/button/button.stories.tsx +0 -150
- package/dist/frontend-core/template/src/components/ui/button/button.tsx +0 -68
- package/dist/frontend-core/template/src/components/ui/calendar/calendar.stories.tsx +0 -160
- package/dist/frontend-core/template/src/components/ui/calendar/calendar.tsx +0 -293
- package/dist/frontend-core/template/src/components/ui/card/card.stories.tsx +0 -77
- package/dist/frontend-core/template/src/components/ui/card/card.tsx +0 -45
- package/dist/frontend-core/template/src/components/ui/card-hover/card-hover.stories.tsx +0 -29
- package/dist/frontend-core/template/src/components/ui/card-hover/card-hover.tsx +0 -28
- package/dist/frontend-core/template/src/components/ui/carousel/carousel.stories.tsx +0 -154
- package/dist/frontend-core/template/src/components/ui/carousel/carousel.tsx +0 -227
- package/dist/frontend-core/template/src/components/ui/checkbox/checkbox.stories.tsx +0 -106
- package/dist/frontend-core/template/src/components/ui/checkbox/checkbox.tsx +0 -88
- package/dist/frontend-core/template/src/components/ui/checkbox/shadcn-checkbox.stories.tsx +0 -90
- package/dist/frontend-core/template/src/components/ui/checkbox/shadcn-checkbox.tsx +0 -54
- package/dist/frontend-core/template/src/components/ui/collapse/collapse.stories.tsx +0 -52
- package/dist/frontend-core/template/src/components/ui/collapse/collapse.tsx +0 -9
- package/dist/frontend-core/template/src/components/ui/combobox/combobox.stories.tsx +0 -207
- package/dist/frontend-core/template/src/components/ui/combobox/combobox.tsx +0 -79
- package/dist/frontend-core/template/src/components/ui/command/command.stories.tsx +0 -186
- package/dist/frontend-core/template/src/components/ui/command/command.tsx +0 -165
- package/dist/frontend-core/template/src/components/ui/command-palette/command-palette.stories.tsx +0 -160
- package/dist/frontend-core/template/src/components/ui/command-palette/command-palette.tsx +0 -134
- package/dist/frontend-core/template/src/components/ui/content-frame/content-frame.stories.tsx +0 -198
- package/dist/frontend-core/template/src/components/ui/content-frame/content-frame.tsx +0 -100
- package/dist/frontend-core/template/src/components/ui/context-menu/context-menu.stories.tsx +0 -78
- package/dist/frontend-core/template/src/components/ui/context-menu/context-menu.tsx +0 -179
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/cell-variant-types.ts +0 -11
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/checkbox-cell.tsx +0 -116
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/date-cell.tsx +0 -157
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/gantt-cell.tsx +0 -82
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/long-text-cell.tsx +0 -180
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/multi-select-cell.tsx +0 -280
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/number-cell.tsx +0 -169
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/react-node-cell.tsx +0 -33
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/select-cell.tsx +0 -175
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/short-text-cell.tsx +0 -138
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/utils/gantt-timeline.tsx +0 -92
- package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/utils/gantt-timerange-picker.tsx +0 -330
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-cell-wrapper.tsx +0 -212
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-cell.tsx +0 -157
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-column-header.tsx +0 -340
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-context-menu.tsx +0 -271
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-row.tsx +0 -123
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-search.tsx +0 -211
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-types.ts +0 -159
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-utils.ts +0 -67
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-view-menu.tsx +0 -360
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid.stories.tsx +0 -780
- package/dist/frontend-core/template/src/components/ui/data-grid/data-grid.tsx +0 -217
- package/dist/frontend-core/template/src/components/ui/data-grid/hooks/use-callback-ref.ts +0 -22
- package/dist/frontend-core/template/src/components/ui/data-grid/hooks/use-data-grid.tsx +0 -1892
- package/dist/frontend-core/template/src/components/ui/data-grid/hooks/use-debounced-callback.ts +0 -19
- package/dist/frontend-core/template/src/components/ui/data-grid/styles.css +0 -3
- package/dist/frontend-core/template/src/components/ui/data-table/context-menu-simple.tsx +0 -141
- package/dist/frontend-core/template/src/components/ui/data-table/data-table.stories.tsx +0 -146
- package/dist/frontend-core/template/src/components/ui/data-table/data-table.tsx +0 -447
- package/dist/frontend-core/template/src/components/ui/data-table/renderers/country-array-cell-renderer.tsx +0 -77
- package/dist/frontend-core/template/src/components/ui/data-table/renderers/country-cell-renderer.tsx +0 -56
- package/dist/frontend-core/template/src/components/ui/data-table/renderers/favorite-cell-renderer.tsx +0 -68
- package/dist/frontend-core/template/src/components/ui/data-table/renderers/links-cell-renderer.tsx +0 -205
- package/dist/frontend-core/template/src/components/ui/data-table/utils/columns.ts +0 -351
- package/dist/frontend-core/template/src/components/ui/data-table/utils/data-table.utils.ts +0 -49
- package/dist/frontend-core/template/src/components/ui/date-picker/date-picker.stories.tsx +0 -149
- package/dist/frontend-core/template/src/components/ui/date-picker/date-picker.tsx +0 -30
- package/dist/frontend-core/template/src/components/ui/dialog/dialog.stories.tsx +0 -80
- package/dist/frontend-core/template/src/components/ui/dialog/dialog.tsx +0 -134
- package/dist/frontend-core/template/src/components/ui/drawer/drawer.stories.tsx +0 -104
- package/dist/frontend-core/template/src/components/ui/drawer/drawer.tsx +0 -87
- package/dist/frontend-core/template/src/components/ui/dropdown-menu/dropdown-menu.stories.tsx +0 -168
- package/dist/frontend-core/template/src/components/ui/dropdown-menu/dropdown-menu.tsx +0 -225
- package/dist/frontend-core/template/src/components/ui/input/input.stories.tsx +0 -141
- package/dist/frontend-core/template/src/components/ui/input/input.tsx +0 -47
- package/dist/frontend-core/template/src/components/ui/label/label.stories.tsx +0 -41
- package/dist/frontend-core/template/src/components/ui/label/label.tsx +0 -20
- package/dist/frontend-core/template/src/components/ui/loader/loader.stories.tsx +0 -45
- package/dist/frontend-core/template/src/components/ui/loader/loader.tsx +0 -17
- package/dist/frontend-core/template/src/components/ui/mark-value-renderer/mark-value-renderer.stories.tsx +0 -114
- package/dist/frontend-core/template/src/components/ui/mark-value-renderer/mark-value-renderer.tsx +0 -48
- package/dist/frontend-core/template/src/components/ui/menubar/menu.stories.tsx +0 -134
- package/dist/frontend-core/template/src/components/ui/menubar/menubar.tsx +0 -208
- package/dist/frontend-core/template/src/components/ui/modal/modal.stories.tsx +0 -297
- package/dist/frontend-core/template/src/components/ui/modal/modal.tsx +0 -80
- package/dist/frontend-core/template/src/components/ui/navigation-menu/navigation-menu.stories.tsx +0 -213
- package/dist/frontend-core/template/src/components/ui/navigation-menu/navigation-menu.tsx +0 -142
- package/dist/frontend-core/template/src/components/ui/pagination/pagination.stories.tsx +0 -49
- package/dist/frontend-core/template/src/components/ui/pagination/pagination.tsx +0 -84
- package/dist/frontend-core/template/src/components/ui/popover/popover.stories.tsx +0 -82
- package/dist/frontend-core/template/src/components/ui/popover/popover.tsx +0 -55
- package/dist/frontend-core/template/src/components/ui/progress/progress.stories.tsx +0 -80
- package/dist/frontend-core/template/src/components/ui/progress/progress.tsx +0 -17
- package/dist/frontend-core/template/src/components/ui/radio-group/radio-group.stories.tsx +0 -154
- package/dist/frontend-core/template/src/components/ui/radio-group/radio-group.tsx +0 -68
- package/dist/frontend-core/template/src/components/ui/resizable/resizable.stories.tsx +0 -73
- package/dist/frontend-core/template/src/components/ui/resizable/resizeable.tsx +0 -38
- package/dist/frontend-core/template/src/components/ui/scroll-area/scroll-area.stories.tsx +0 -55
- package/dist/frontend-core/template/src/components/ui/scroll-area/scroll-area.tsx +0 -39
- package/dist/frontend-core/template/src/components/ui/select/select.stories.tsx +0 -297
- package/dist/frontend-core/template/src/components/ui/select/select.tsx +0 -227
- package/dist/frontend-core/template/src/components/ui/separator/separator.tsx +0 -21
- package/dist/frontend-core/template/src/components/ui/separator/seperator.stories.tsx +0 -25
- package/dist/frontend-core/template/src/components/ui/sheet/sheet.stories.tsx +0 -45
- package/dist/frontend-core/template/src/components/ui/sheet/sheet.tsx +0 -107
- package/dist/frontend-core/template/src/components/ui/skeleton/skeleton.stories.tsx +0 -26
- package/dist/frontend-core/template/src/components/ui/skeleton/skeleton.tsx +0 -7
- package/dist/frontend-core/template/src/components/ui/slider/slider.stories.tsx +0 -101
- package/dist/frontend-core/template/src/components/ui/slider/slider.tsx +0 -98
- package/dist/frontend-core/template/src/components/ui/spinner/spinner.stories.tsx +0 -19
- package/dist/frontend-core/template/src/components/ui/spinner/spinner.tsx +0 -21
- package/dist/frontend-core/template/src/components/ui/switch/switch.stories.tsx +0 -33
- package/dist/frontend-core/template/src/components/ui/switch/switch.tsx +0 -28
- package/dist/frontend-core/template/src/components/ui/tabs/tabs.stories.tsx +0 -215
- package/dist/frontend-core/template/src/components/ui/tabs/tabs.tsx +0 -70
- package/dist/frontend-core/template/src/components/ui/textarea/textarea.stories.tsx +0 -138
- package/dist/frontend-core/template/src/components/ui/textarea/textarea.tsx +0 -40
- package/dist/frontend-core/template/src/components/ui/toast/toast.mdx +0 -31
- package/dist/frontend-core/template/src/components/ui/toast/toast.stories.tsx +0 -89
- package/dist/frontend-core/template/src/components/ui/toggle/toggle.stories.tsx +0 -65
- package/dist/frontend-core/template/src/components/ui/toggle/toggle.tsx +0 -38
- package/dist/frontend-core/template/src/components/ui/toggle-group/toggle-group.stories.tsx +0 -85
- package/dist/frontend-core/template/src/components/ui/toggle-group/toggle-group.tsx +0 -54
- package/dist/frontend-core/template/src/components/ui/tooltip/tooltip.stories.tsx +0 -29
- package/dist/frontend-core/template/src/components/ui/tooltip/tooltip.tsx +0 -29
|
@@ -1,1892 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ColumnDef,
|
|
3
|
-
getCoreRowModel,
|
|
4
|
-
getSortedRowModel,
|
|
5
|
-
type RowSelectionState,
|
|
6
|
-
type SortingState,
|
|
7
|
-
type TableOptions,
|
|
8
|
-
type Updater,
|
|
9
|
-
useReactTable,
|
|
10
|
-
} from '@tanstack/react-table'
|
|
11
|
-
import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual'
|
|
12
|
-
|
|
13
|
-
import * as React from 'react'
|
|
14
|
-
|
|
15
|
-
import { DataGridCell } from '@components/ui/data-grid/data-grid-cell'
|
|
16
|
-
import type {
|
|
17
|
-
CellPosition,
|
|
18
|
-
ContextMenuState,
|
|
19
|
-
NavigationDirection,
|
|
20
|
-
RowHeightValue,
|
|
21
|
-
SearchState,
|
|
22
|
-
SelectionState,
|
|
23
|
-
UpdateCell,
|
|
24
|
-
} from '@components/ui/data-grid/data-grid-types'
|
|
25
|
-
import { getCellKey, getRowHeightValue, parseCellKey } from '@components/ui/data-grid/data-grid-utils'
|
|
26
|
-
|
|
27
|
-
const DEFAULT_ROW_HEIGHT = 'short'
|
|
28
|
-
const OVERSCAN = 3
|
|
29
|
-
const VIEWPORT_OFFSET = 1
|
|
30
|
-
const MIN_COLUMN_SIZE = 60
|
|
31
|
-
const MAX_COLUMN_SIZE = 800
|
|
32
|
-
const SEARCH_SHORTCUT_KEY = 'f'
|
|
33
|
-
const NON_NAVIGABLE_COLUMN_IDS = ['select', 'actions']
|
|
34
|
-
|
|
35
|
-
const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
|
|
36
|
-
|
|
37
|
-
function useLazyRef<T>(fn: () => T): React.RefObject<T> {
|
|
38
|
-
const ref = React.useRef<T | null>(null)
|
|
39
|
-
if (ref.current === null) {
|
|
40
|
-
ref.current = fn()
|
|
41
|
-
}
|
|
42
|
-
return ref as React.RefObject<T>
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function useAsRef<T>(data: T) {
|
|
46
|
-
const ref = React.useRef<T>(data)
|
|
47
|
-
|
|
48
|
-
useIsomorphicLayoutEffect(() => {
|
|
49
|
-
ref.current = data
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
return ref
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type DataGridState = {
|
|
56
|
-
sorting: SortingState
|
|
57
|
-
rowHeight: RowHeightValue
|
|
58
|
-
rowSelection: RowSelectionState
|
|
59
|
-
selectionState: SelectionState
|
|
60
|
-
focusedCell: CellPosition | null
|
|
61
|
-
editingCell: CellPosition | null
|
|
62
|
-
contextMenu: ContextMenuState
|
|
63
|
-
searchQuery: string
|
|
64
|
-
searchMatches: CellPosition[]
|
|
65
|
-
matchIndex: number
|
|
66
|
-
searchOpen: boolean
|
|
67
|
-
lastClickedRowIndex: number | null
|
|
68
|
-
isScrolling: boolean
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
type DataGridStore = {
|
|
72
|
-
subscribe: (callback: () => void) => () => void
|
|
73
|
-
getState: () => DataGridState
|
|
74
|
-
setState: <K extends keyof DataGridState>(key: K, value: DataGridState[K]) => void
|
|
75
|
-
notify: () => void
|
|
76
|
-
batch: (fn: () => void) => void
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function useStore<T>(store: DataGridStore, selector: (state: DataGridState) => T): T {
|
|
80
|
-
const getSnapshot = React.useCallback(() => selector(store.getState()), [store, selector])
|
|
81
|
-
|
|
82
|
-
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
type UseDataGridProps<TData> = {
|
|
86
|
-
onDataChange?: (data: TData[]) => void
|
|
87
|
-
onRowAdd?: (event?: React.MouseEvent<HTMLDivElement>) =>
|
|
88
|
-
| Partial<CellPosition>
|
|
89
|
-
| Promise<Partial<CellPosition>>
|
|
90
|
-
| null
|
|
91
|
-
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
92
|
-
| void // void is needed here to allow functions without explicit return
|
|
93
|
-
onRowsDelete?: (rows: TData[], rowIndices: number[]) => void | Promise<void>
|
|
94
|
-
onCellFocus?: (args: { rowIndex: number; columnId: string }) => void
|
|
95
|
-
rowHeight?: RowHeightValue
|
|
96
|
-
overscan?: number
|
|
97
|
-
autoFocus?: boolean | Partial<CellPosition>
|
|
98
|
-
enableColumnSelection?: boolean
|
|
99
|
-
enableSearch?: boolean
|
|
100
|
-
} & Omit<TableOptions<TData>, 'pageCount' | 'getCoreRowModel'>
|
|
101
|
-
|
|
102
|
-
function useDataGrid<TData>({
|
|
103
|
-
columns,
|
|
104
|
-
data,
|
|
105
|
-
onDataChange,
|
|
106
|
-
onRowAdd: onRowAddProp,
|
|
107
|
-
onRowsDelete: onRowsDeleteProp,
|
|
108
|
-
onCellFocus: onCellFocusProp,
|
|
109
|
-
rowHeight: rowHeightProp = DEFAULT_ROW_HEIGHT,
|
|
110
|
-
overscan = OVERSCAN,
|
|
111
|
-
initialState,
|
|
112
|
-
autoFocus = false,
|
|
113
|
-
enableColumnSelection = false,
|
|
114
|
-
enableSearch = false,
|
|
115
|
-
...dataGridProps
|
|
116
|
-
}: UseDataGridProps<TData>) {
|
|
117
|
-
const dataGridRef = React.useRef<HTMLDivElement>(null)
|
|
118
|
-
const tableRef = React.useRef<ReturnType<typeof useReactTable<TData>>>(null)
|
|
119
|
-
const rowVirtualizerRef = React.useRef<Virtualizer<HTMLDivElement, Element>>(null)
|
|
120
|
-
const headerRef = React.useRef<HTMLDivElement>(null)
|
|
121
|
-
const rowMapRef = React.useRef<Map<number, HTMLDivElement>>(new Map())
|
|
122
|
-
const footerRef = React.useRef<HTMLDivElement>(null)
|
|
123
|
-
|
|
124
|
-
const dataGridPropsRef = useAsRef(dataGridProps)
|
|
125
|
-
const listenersRef = useLazyRef(() => new Set<() => void>())
|
|
126
|
-
|
|
127
|
-
const stateRef = useLazyRef<DataGridState>(() => {
|
|
128
|
-
return {
|
|
129
|
-
sorting: initialState?.sorting ?? [],
|
|
130
|
-
rowHeight: rowHeightProp,
|
|
131
|
-
rowSelection: initialState?.rowSelection ?? {},
|
|
132
|
-
selectionState: {
|
|
133
|
-
selectedCells: new Set(),
|
|
134
|
-
selectionRange: null,
|
|
135
|
-
isSelecting: false,
|
|
136
|
-
},
|
|
137
|
-
focusedCell: null,
|
|
138
|
-
editingCell: null,
|
|
139
|
-
contextMenu: {
|
|
140
|
-
open: false,
|
|
141
|
-
x: 0,
|
|
142
|
-
y: 0,
|
|
143
|
-
},
|
|
144
|
-
searchQuery: '',
|
|
145
|
-
searchMatches: [],
|
|
146
|
-
matchIndex: -1,
|
|
147
|
-
searchOpen: false,
|
|
148
|
-
lastClickedRowIndex: null,
|
|
149
|
-
isScrolling: false,
|
|
150
|
-
}
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
const store = React.useMemo<DataGridStore>(() => {
|
|
154
|
-
let isBatching = false
|
|
155
|
-
let pendingNotification = false
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
subscribe: (callback) => {
|
|
159
|
-
listenersRef.current.add(callback)
|
|
160
|
-
return () => listenersRef.current.delete(callback)
|
|
161
|
-
},
|
|
162
|
-
getState: () => stateRef.current,
|
|
163
|
-
setState: (key, value) => {
|
|
164
|
-
if (Object.is(stateRef.current[key], value)) {
|
|
165
|
-
return
|
|
166
|
-
}
|
|
167
|
-
stateRef.current[key] = value
|
|
168
|
-
|
|
169
|
-
if (isBatching) {
|
|
170
|
-
pendingNotification = true
|
|
171
|
-
} else {
|
|
172
|
-
if (!pendingNotification) {
|
|
173
|
-
pendingNotification = true
|
|
174
|
-
queueMicrotask(() => {
|
|
175
|
-
pendingNotification = false
|
|
176
|
-
store.notify()
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
notify: () => {
|
|
182
|
-
for (const listener of listenersRef.current) {
|
|
183
|
-
listener()
|
|
184
|
-
}
|
|
185
|
-
},
|
|
186
|
-
batch: (fn) => {
|
|
187
|
-
if (isBatching) {
|
|
188
|
-
fn()
|
|
189
|
-
return
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
isBatching = true
|
|
193
|
-
const wasPending = pendingNotification
|
|
194
|
-
pendingNotification = false
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
fn()
|
|
198
|
-
} finally {
|
|
199
|
-
isBatching = false
|
|
200
|
-
if (pendingNotification || wasPending) {
|
|
201
|
-
pendingNotification = false
|
|
202
|
-
store.notify()
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
},
|
|
206
|
-
}
|
|
207
|
-
}, [listenersRef, stateRef])
|
|
208
|
-
|
|
209
|
-
React.useEffect(() => {
|
|
210
|
-
store.setState('rowHeight', rowHeightProp)
|
|
211
|
-
}, [rowHeightProp, store])
|
|
212
|
-
|
|
213
|
-
const focusedCell = useStore(store, (state) => state.focusedCell)
|
|
214
|
-
const editingCell = useStore(store, (state) => state.editingCell)
|
|
215
|
-
const selectionState = useStore(store, (state) => state.selectionState)
|
|
216
|
-
const searchQuery = useStore(store, (state) => state.searchQuery)
|
|
217
|
-
const searchMatches = useStore(store, (state) => state.searchMatches)
|
|
218
|
-
const matchIndex = useStore(store, (state) => state.matchIndex)
|
|
219
|
-
const searchOpen = useStore(store, (state) => state.searchOpen)
|
|
220
|
-
const sorting = useStore(store, (state) => state.sorting)
|
|
221
|
-
const rowSelection = useStore(store, (state) => state.rowSelection)
|
|
222
|
-
const contextMenu = useStore(store, (state) => state.contextMenu)
|
|
223
|
-
const rowHeight = useStore(store, (state) => state.rowHeight)
|
|
224
|
-
const isScrolling = useStore(store, (state) => state.isScrolling)
|
|
225
|
-
|
|
226
|
-
const rowHeightValue = getRowHeightValue(rowHeight)
|
|
227
|
-
|
|
228
|
-
const columnIds = React.useMemo(() => {
|
|
229
|
-
return columns
|
|
230
|
-
.map((c) => {
|
|
231
|
-
if (c.id) {
|
|
232
|
-
return c.id
|
|
233
|
-
}
|
|
234
|
-
if ('accessorKey' in c) {
|
|
235
|
-
return c.accessorKey as string
|
|
236
|
-
}
|
|
237
|
-
return undefined
|
|
238
|
-
})
|
|
239
|
-
.filter((id): id is string => Boolean(id))
|
|
240
|
-
}, [columns])
|
|
241
|
-
|
|
242
|
-
// Build a storage key for this table. We use the current pathname plus the column ids so different tables/pages get separate storage slots
|
|
243
|
-
const storageKey = React.useMemo(() => {
|
|
244
|
-
if (typeof window === 'undefined') {
|
|
245
|
-
return undefined
|
|
246
|
-
}
|
|
247
|
-
const path = window.location.pathname || 'unknown'
|
|
248
|
-
const cols = columnIds.join('|')
|
|
249
|
-
return `pxl.dataGrid:${path}:${cols}`
|
|
250
|
-
}, [columnIds])
|
|
251
|
-
|
|
252
|
-
// Try to load persisted table state (order/visibility/pinning) from localStorage and merge it into the initial state the table will be created with.
|
|
253
|
-
const persistedState = React.useMemo(() => {
|
|
254
|
-
if (!storageKey) {
|
|
255
|
-
return undefined
|
|
256
|
-
}
|
|
257
|
-
try {
|
|
258
|
-
const raw = localStorage.getItem(storageKey)
|
|
259
|
-
if (!raw) {
|
|
260
|
-
return undefined
|
|
261
|
-
}
|
|
262
|
-
const parsed = JSON.parse(raw)
|
|
263
|
-
return parsed
|
|
264
|
-
} catch (_) {
|
|
265
|
-
return undefined
|
|
266
|
-
}
|
|
267
|
-
}, [storageKey])
|
|
268
|
-
|
|
269
|
-
// Merge persisted state (if any) into the table initial state so stored column order / visibility / pinning get reapplied on load.
|
|
270
|
-
const mergedInitialState = React.useMemo(() => {
|
|
271
|
-
const base = { ...(initialState ?? {}) } as Record<string, unknown>
|
|
272
|
-
|
|
273
|
-
if (persistedState) {
|
|
274
|
-
try {
|
|
275
|
-
if (persistedState.columnOrder) {
|
|
276
|
-
base.columnOrder = persistedState.columnOrder
|
|
277
|
-
}
|
|
278
|
-
if (persistedState.columnVisibility) {
|
|
279
|
-
base.columnVisibility = persistedState.columnVisibility
|
|
280
|
-
}
|
|
281
|
-
if (persistedState.columnPinning) {
|
|
282
|
-
base.columnPinning = persistedState.columnPinning
|
|
283
|
-
}
|
|
284
|
-
} catch (_) {
|
|
285
|
-
// ignore malformed persisted state
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return base as typeof initialState
|
|
290
|
-
}, [initialState, persistedState])
|
|
291
|
-
|
|
292
|
-
// Derive the current visible, ordered column ids from the table when available.
|
|
293
|
-
// This respects `table.setColumnOrder(...)` and column visibility.
|
|
294
|
-
const getNavigableColumnIds = React.useCallback(() => {
|
|
295
|
-
const t = tableRef.current
|
|
296
|
-
if (t) {
|
|
297
|
-
return t
|
|
298
|
-
.getVisibleLeafColumns()
|
|
299
|
-
.map((c) => c.id)
|
|
300
|
-
.filter((c) => !NON_NAVIGABLE_COLUMN_IDS.includes(c))
|
|
301
|
-
}
|
|
302
|
-
return columnIds.filter((c) => !NON_NAVIGABLE_COLUMN_IDS.includes(c))
|
|
303
|
-
}, [columnIds])
|
|
304
|
-
|
|
305
|
-
const onDataUpdate = React.useCallback(
|
|
306
|
-
(updates: UpdateCell | UpdateCell[]) => {
|
|
307
|
-
const updateArray = Array.isArray(updates) ? updates : [updates]
|
|
308
|
-
|
|
309
|
-
if (updateArray.length === 0) {
|
|
310
|
-
return
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const currentTable = tableRef.current
|
|
314
|
-
const rows = currentTable?.getRowModel().rows
|
|
315
|
-
|
|
316
|
-
const rowUpdatesMap = new Map<number, Omit<UpdateCell, 'rowIndex'>[]>()
|
|
317
|
-
|
|
318
|
-
for (const update of updateArray) {
|
|
319
|
-
if (!rows || !currentTable) {
|
|
320
|
-
const existingUpdates = rowUpdatesMap.get(update.rowIndex) ?? []
|
|
321
|
-
existingUpdates.push({
|
|
322
|
-
columnId: update.columnId,
|
|
323
|
-
value: update.value,
|
|
324
|
-
})
|
|
325
|
-
rowUpdatesMap.set(update.rowIndex, existingUpdates)
|
|
326
|
-
} else {
|
|
327
|
-
const row = rows[update.rowIndex]
|
|
328
|
-
if (!row) {
|
|
329
|
-
continue
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const originalData = row.original
|
|
333
|
-
const originalRowIndex = data.indexOf(originalData)
|
|
334
|
-
if (originalRowIndex === -1) {
|
|
335
|
-
continue
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const existingUpdates = rowUpdatesMap.get(originalRowIndex) ?? []
|
|
339
|
-
existingUpdates.push({
|
|
340
|
-
columnId: update.columnId,
|
|
341
|
-
value: update.value,
|
|
342
|
-
})
|
|
343
|
-
rowUpdatesMap.set(originalRowIndex, existingUpdates)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
// Also notify any per-cell change handler if present on the table meta.
|
|
347
|
-
const currentMeta = tableRef.current?.options.meta
|
|
348
|
-
if (currentMeta?.onCellChange) {
|
|
349
|
-
for (const u of updateArray) {
|
|
350
|
-
try {
|
|
351
|
-
currentMeta.onCellChange(u)
|
|
352
|
-
} catch (_) {
|
|
353
|
-
// Ignore handler errors here to avoid breaking the grid behavior
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
const newData = data.map((row, index) => {
|
|
359
|
-
const updates = rowUpdatesMap.get(index)
|
|
360
|
-
if (!updates) {
|
|
361
|
-
return row
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
const updatedRow = { ...row } as Record<string, unknown>
|
|
365
|
-
for (const { columnId, value } of updates) {
|
|
366
|
-
updatedRow[columnId] = value
|
|
367
|
-
}
|
|
368
|
-
return updatedRow as TData
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
onDataChange?.(newData)
|
|
372
|
-
},
|
|
373
|
-
[data, onDataChange],
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
const getIsCellSelected = React.useCallback(
|
|
377
|
-
(rowIndex: number, columnId: string) => {
|
|
378
|
-
return selectionState.selectedCells.has(getCellKey(rowIndex, columnId))
|
|
379
|
-
},
|
|
380
|
-
[selectionState.selectedCells],
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
const clearSelection = React.useCallback(() => {
|
|
384
|
-
store.batch(() => {
|
|
385
|
-
store.setState('selectionState', {
|
|
386
|
-
selectedCells: new Set(),
|
|
387
|
-
selectionRange: null,
|
|
388
|
-
isSelecting: false,
|
|
389
|
-
})
|
|
390
|
-
store.setState('rowSelection', {})
|
|
391
|
-
})
|
|
392
|
-
}, [store])
|
|
393
|
-
|
|
394
|
-
const selectAll = React.useCallback(() => {
|
|
395
|
-
const allCells = new Set<string>()
|
|
396
|
-
const currentTable = tableRef.current
|
|
397
|
-
const rows = currentTable?.getRowModel().rows ?? []
|
|
398
|
-
const rowCount = rows.length ?? data.length
|
|
399
|
-
|
|
400
|
-
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
401
|
-
for (const columnId of columnIds) {
|
|
402
|
-
allCells.add(getCellKey(rowIndex, columnId))
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const firstColumnId = columnIds[0]
|
|
407
|
-
const lastColumnId = columnIds[columnIds.length - 1]
|
|
408
|
-
|
|
409
|
-
store.setState('selectionState', {
|
|
410
|
-
selectedCells: allCells,
|
|
411
|
-
selectionRange:
|
|
412
|
-
columnIds.length > 0 && rowCount > 0 && firstColumnId && lastColumnId
|
|
413
|
-
? {
|
|
414
|
-
start: { rowIndex: 0, columnId: firstColumnId },
|
|
415
|
-
end: { rowIndex: rowCount - 1, columnId: lastColumnId },
|
|
416
|
-
}
|
|
417
|
-
: null,
|
|
418
|
-
isSelecting: false,
|
|
419
|
-
})
|
|
420
|
-
}, [columnIds, data.length, store])
|
|
421
|
-
|
|
422
|
-
const selectColumn = React.useCallback(
|
|
423
|
-
(columnId: string) => {
|
|
424
|
-
const currentTable = tableRef.current
|
|
425
|
-
const rows = currentTable?.getRowModel().rows ?? []
|
|
426
|
-
const rowCount = rows.length ?? data.length
|
|
427
|
-
|
|
428
|
-
if (rowCount === 0) {
|
|
429
|
-
return
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const selectedCells = new Set<string>()
|
|
433
|
-
|
|
434
|
-
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
435
|
-
selectedCells.add(getCellKey(rowIndex, columnId))
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
store.setState('selectionState', {
|
|
439
|
-
selectedCells,
|
|
440
|
-
selectionRange: {
|
|
441
|
-
start: { rowIndex: 0, columnId },
|
|
442
|
-
end: { rowIndex: rowCount - 1, columnId },
|
|
443
|
-
},
|
|
444
|
-
isSelecting: false,
|
|
445
|
-
})
|
|
446
|
-
},
|
|
447
|
-
[data.length, store],
|
|
448
|
-
)
|
|
449
|
-
|
|
450
|
-
const selectRange = React.useCallback(
|
|
451
|
-
(start: CellPosition, end: CellPosition, isSelecting = false) => {
|
|
452
|
-
// Use the current visible/displayed column order so drag selection
|
|
453
|
-
// follows the UI when users reorder columns.
|
|
454
|
-
const visibleCols = getNavigableColumnIds()
|
|
455
|
-
const startColIndex = visibleCols.indexOf(start.columnId)
|
|
456
|
-
const endColIndex = visibleCols.indexOf(end.columnId)
|
|
457
|
-
|
|
458
|
-
const minRow = Math.min(start.rowIndex, end.rowIndex)
|
|
459
|
-
const maxRow = Math.max(start.rowIndex, end.rowIndex)
|
|
460
|
-
const minCol = Math.min(startColIndex, endColIndex)
|
|
461
|
-
const maxCol = Math.max(startColIndex, endColIndex)
|
|
462
|
-
|
|
463
|
-
const selectedCells = new Set<string>()
|
|
464
|
-
|
|
465
|
-
for (let rowIndex = minRow; rowIndex <= maxRow; rowIndex++) {
|
|
466
|
-
for (let colIndex = minCol; colIndex <= maxCol; colIndex++) {
|
|
467
|
-
const columnId = visibleCols[colIndex]
|
|
468
|
-
if (columnId) {
|
|
469
|
-
selectedCells.add(getCellKey(rowIndex, columnId))
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
store.setState('selectionState', {
|
|
475
|
-
selectedCells,
|
|
476
|
-
selectionRange: { start, end },
|
|
477
|
-
isSelecting,
|
|
478
|
-
})
|
|
479
|
-
},
|
|
480
|
-
[getNavigableColumnIds, store],
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
const focusCell = React.useCallback(
|
|
484
|
-
(rowIndex: number, columnId: string) => {
|
|
485
|
-
store.batch(() => {
|
|
486
|
-
store.setState('focusedCell', { rowIndex, columnId })
|
|
487
|
-
store.setState('editingCell', null)
|
|
488
|
-
})
|
|
489
|
-
|
|
490
|
-
// Notify parent component about cell focus change
|
|
491
|
-
onCellFocusProp?.({ rowIndex, columnId })
|
|
492
|
-
|
|
493
|
-
const currentState = store.getState()
|
|
494
|
-
|
|
495
|
-
if (currentState.searchOpen) {
|
|
496
|
-
return
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
if (dataGridRef.current && document.activeElement !== dataGridRef.current) {
|
|
500
|
-
dataGridRef.current.focus()
|
|
501
|
-
}
|
|
502
|
-
},
|
|
503
|
-
[store, onCellFocusProp],
|
|
504
|
-
)
|
|
505
|
-
|
|
506
|
-
const onRowsDelete = React.useCallback(
|
|
507
|
-
async (rowIndices: number[]) => {
|
|
508
|
-
if (!onRowsDeleteProp || rowIndices.length === 0) {
|
|
509
|
-
return
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const currentTable = tableRef.current
|
|
513
|
-
const rows = currentTable?.getRowModel().rows
|
|
514
|
-
|
|
515
|
-
if (!rows || rows.length === 0) {
|
|
516
|
-
return
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const currentState = store.getState()
|
|
520
|
-
const currentFocusedColumn = currentState.focusedCell?.columnId ?? getNavigableColumnIds()[0]
|
|
521
|
-
|
|
522
|
-
const minDeletedRowIndex = Math.min(...rowIndices)
|
|
523
|
-
|
|
524
|
-
const rowsToDelete: TData[] = []
|
|
525
|
-
for (const rowIndex of rowIndices) {
|
|
526
|
-
const row = rows[rowIndex]
|
|
527
|
-
if (row) {
|
|
528
|
-
rowsToDelete.push(row.original)
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
await onRowsDeleteProp(rowsToDelete, rowIndices)
|
|
533
|
-
|
|
534
|
-
store.batch(() => {
|
|
535
|
-
store.setState('selectionState', {
|
|
536
|
-
selectedCells: new Set(),
|
|
537
|
-
selectionRange: null,
|
|
538
|
-
isSelecting: false,
|
|
539
|
-
})
|
|
540
|
-
store.setState('rowSelection', {})
|
|
541
|
-
store.setState('editingCell', null)
|
|
542
|
-
})
|
|
543
|
-
|
|
544
|
-
requestAnimationFrame(() => {
|
|
545
|
-
const currentTable = tableRef.current
|
|
546
|
-
const currentRows = currentTable?.getRowModel().rows ?? []
|
|
547
|
-
const newRowCount = currentRows.length ?? data.length
|
|
548
|
-
|
|
549
|
-
if (newRowCount > 0 && currentFocusedColumn) {
|
|
550
|
-
const targetRowIndex = Math.min(minDeletedRowIndex, newRowCount - 1)
|
|
551
|
-
focusCell(targetRowIndex, currentFocusedColumn)
|
|
552
|
-
}
|
|
553
|
-
})
|
|
554
|
-
},
|
|
555
|
-
[onRowsDeleteProp, data.length, store, getNavigableColumnIds, focusCell],
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
const navigateCell = React.useCallback(
|
|
559
|
-
(direction: NavigationDirection) => {
|
|
560
|
-
const currentState = store.getState()
|
|
561
|
-
if (!currentState.focusedCell) {
|
|
562
|
-
return
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
const { rowIndex, columnId } = currentState.focusedCell
|
|
566
|
-
const _navCols = getNavigableColumnIds()
|
|
567
|
-
const currentColIndex = _navCols.indexOf(columnId)
|
|
568
|
-
const rowVirtualizer = rowVirtualizerRef.current
|
|
569
|
-
const currentTable = tableRef.current
|
|
570
|
-
const rows = currentTable?.getRowModel().rows ?? []
|
|
571
|
-
const rowCount = rows.length ?? data.length
|
|
572
|
-
|
|
573
|
-
let newRowIndex = rowIndex
|
|
574
|
-
let newColumnId = columnId
|
|
575
|
-
|
|
576
|
-
switch (direction) {
|
|
577
|
-
case 'up':
|
|
578
|
-
newRowIndex = Math.max(0, rowIndex - 1)
|
|
579
|
-
break
|
|
580
|
-
case 'down':
|
|
581
|
-
newRowIndex = Math.min(rowCount - 1, rowIndex + 1)
|
|
582
|
-
break
|
|
583
|
-
case 'left':
|
|
584
|
-
if (currentColIndex > 0) {
|
|
585
|
-
const prevColumnId = _navCols[currentColIndex - 1]
|
|
586
|
-
if (prevColumnId) {
|
|
587
|
-
newColumnId = prevColumnId
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
break
|
|
591
|
-
case 'right':
|
|
592
|
-
if (currentColIndex < _navCols.length - 1) {
|
|
593
|
-
const nextColumnId = _navCols[currentColIndex + 1]
|
|
594
|
-
if (nextColumnId) {
|
|
595
|
-
newColumnId = nextColumnId
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
break
|
|
599
|
-
case 'home': {
|
|
600
|
-
const _navCols2 = getNavigableColumnIds()
|
|
601
|
-
if (_navCols2.length > 0) {
|
|
602
|
-
newColumnId = _navCols2[0] ?? columnId
|
|
603
|
-
}
|
|
604
|
-
break
|
|
605
|
-
}
|
|
606
|
-
case 'end': {
|
|
607
|
-
const _navCols3 = getNavigableColumnIds()
|
|
608
|
-
if (_navCols3.length > 0) {
|
|
609
|
-
newColumnId = _navCols3[_navCols3.length - 1] ?? columnId
|
|
610
|
-
}
|
|
611
|
-
break
|
|
612
|
-
}
|
|
613
|
-
case 'ctrl+home': {
|
|
614
|
-
newRowIndex = 0
|
|
615
|
-
const _navCols4 = getNavigableColumnIds()
|
|
616
|
-
if (_navCols4.length > 0) {
|
|
617
|
-
newColumnId = _navCols4[0] ?? columnId
|
|
618
|
-
}
|
|
619
|
-
break
|
|
620
|
-
}
|
|
621
|
-
case 'ctrl+end': {
|
|
622
|
-
newRowIndex = Math.max(0, rowCount - 1)
|
|
623
|
-
const _navCols5 = getNavigableColumnIds()
|
|
624
|
-
if (_navCols5.length > 0) {
|
|
625
|
-
newColumnId = _navCols5[_navCols5.length - 1] ?? columnId
|
|
626
|
-
}
|
|
627
|
-
break
|
|
628
|
-
}
|
|
629
|
-
case 'pageup':
|
|
630
|
-
if (rowVirtualizer) {
|
|
631
|
-
const visibleRange = rowVirtualizer.getVirtualItems()
|
|
632
|
-
const pageSize = visibleRange.length ?? 10
|
|
633
|
-
newRowIndex = Math.max(0, rowIndex - pageSize)
|
|
634
|
-
} else {
|
|
635
|
-
newRowIndex = Math.max(0, rowIndex - 10)
|
|
636
|
-
}
|
|
637
|
-
break
|
|
638
|
-
case 'pagedown':
|
|
639
|
-
if (rowVirtualizer) {
|
|
640
|
-
const visibleRange = rowVirtualizer.getVirtualItems()
|
|
641
|
-
const pageSize = visibleRange.length ?? 10
|
|
642
|
-
newRowIndex = Math.min(rowCount - 1, rowIndex + pageSize)
|
|
643
|
-
} else {
|
|
644
|
-
newRowIndex = Math.min(rowCount - 1, rowIndex + 10)
|
|
645
|
-
}
|
|
646
|
-
break
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (newRowIndex !== rowIndex || newColumnId !== columnId) {
|
|
650
|
-
const rowDiff = newRowIndex - rowIndex
|
|
651
|
-
|
|
652
|
-
// For single-row vertical navigation (up/down arrows)
|
|
653
|
-
if (Math.abs(rowDiff) === 1 && (direction === 'up' || direction === 'down')) {
|
|
654
|
-
const container = dataGridRef.current
|
|
655
|
-
const currentRow = rowMapRef.current.get(rowIndex)
|
|
656
|
-
const targetRow = rowMapRef.current.get(newRowIndex)
|
|
657
|
-
|
|
658
|
-
if (!container || !currentRow) {
|
|
659
|
-
// Fallback to simple focus if we can't find elements
|
|
660
|
-
focusCell(newRowIndex, newColumnId)
|
|
661
|
-
return
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// Check viewport boundaries
|
|
665
|
-
const containerRect = container.getBoundingClientRect()
|
|
666
|
-
const headerHeight = headerRef.current?.getBoundingClientRect().height ?? 0
|
|
667
|
-
const footerHeight = footerRef.current?.getBoundingClientRect().height ?? 0
|
|
668
|
-
|
|
669
|
-
const viewportTop = containerRect.top + headerHeight + VIEWPORT_OFFSET
|
|
670
|
-
const viewportBottom = containerRect.bottom - footerHeight - VIEWPORT_OFFSET
|
|
671
|
-
|
|
672
|
-
// If target row already exists, check if it's visible
|
|
673
|
-
if (targetRow) {
|
|
674
|
-
const rowRect = targetRow.getBoundingClientRect()
|
|
675
|
-
const isFullyVisible = rowRect.top >= viewportTop && rowRect.bottom <= viewportBottom
|
|
676
|
-
|
|
677
|
-
if (isFullyVisible) {
|
|
678
|
-
// Row is fully visible, just focus it
|
|
679
|
-
focusCell(newRowIndex, newColumnId)
|
|
680
|
-
return
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// Row exists but not fully visible, scroll it into view
|
|
684
|
-
focusCell(newRowIndex, newColumnId)
|
|
685
|
-
|
|
686
|
-
if (direction === 'down') {
|
|
687
|
-
// Scroll just enough to show the row at the bottom
|
|
688
|
-
const scrollNeeded = rowRect.bottom - viewportBottom
|
|
689
|
-
container.scrollTop += scrollNeeded
|
|
690
|
-
} else {
|
|
691
|
-
// Scroll just enough to show the row at the top
|
|
692
|
-
const scrollNeeded = viewportTop - rowRect.top
|
|
693
|
-
container.scrollTop -= scrollNeeded
|
|
694
|
-
}
|
|
695
|
-
return
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
// Target row is not rendered yet
|
|
699
|
-
// Focus immediately so the ring appears as the row is revealed
|
|
700
|
-
focusCell(newRowIndex, newColumnId)
|
|
701
|
-
|
|
702
|
-
// Scroll by exactly one row height to reveal it smoothly
|
|
703
|
-
if (direction === 'down') {
|
|
704
|
-
container.scrollTop += rowHeightValue
|
|
705
|
-
} else {
|
|
706
|
-
// For arrow up, ensure we don't go below 0
|
|
707
|
-
const currentScrollTop = container.scrollTop
|
|
708
|
-
const targetScrollTop = Math.max(0, currentScrollTop - rowHeightValue)
|
|
709
|
-
container.scrollTop = targetScrollTop
|
|
710
|
-
}
|
|
711
|
-
return
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// For larger jumps (page up/down, ctrl+home/end, etc.)
|
|
715
|
-
if (rowVirtualizer && Math.abs(rowDiff) > 1) {
|
|
716
|
-
const align =
|
|
717
|
-
direction === 'pageup' || direction === 'ctrl+home'
|
|
718
|
-
? 'start'
|
|
719
|
-
: direction === 'pagedown' || direction === 'ctrl+end'
|
|
720
|
-
? 'end'
|
|
721
|
-
: 'center'
|
|
722
|
-
rowVirtualizer.scrollToIndex(newRowIndex, { align })
|
|
723
|
-
requestAnimationFrame(() => {
|
|
724
|
-
focusCell(newRowIndex, newColumnId)
|
|
725
|
-
})
|
|
726
|
-
return
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// For horizontal navigation or when row is already visible
|
|
730
|
-
focusCell(newRowIndex, newColumnId)
|
|
731
|
-
}
|
|
732
|
-
},
|
|
733
|
-
[store, getNavigableColumnIds, focusCell, data.length, rowHeightValue],
|
|
734
|
-
)
|
|
735
|
-
|
|
736
|
-
const onCellEditingStart = React.useCallback(
|
|
737
|
-
(rowIndex: number, columnId: string) => {
|
|
738
|
-
store.batch(() => {
|
|
739
|
-
store.setState('focusedCell', { rowIndex, columnId })
|
|
740
|
-
store.setState('editingCell', { rowIndex, columnId })
|
|
741
|
-
})
|
|
742
|
-
},
|
|
743
|
-
[store],
|
|
744
|
-
)
|
|
745
|
-
|
|
746
|
-
const onCellEditingStop = React.useCallback(
|
|
747
|
-
(opts?: { moveToNextRow?: boolean; direction?: NavigationDirection }) => {
|
|
748
|
-
const currentState = store.getState()
|
|
749
|
-
const currentEditing = currentState.editingCell
|
|
750
|
-
|
|
751
|
-
store.setState('editingCell', null)
|
|
752
|
-
|
|
753
|
-
if (opts?.moveToNextRow && currentEditing) {
|
|
754
|
-
const { rowIndex, columnId } = currentEditing
|
|
755
|
-
const currentTable = tableRef.current
|
|
756
|
-
const rows = currentTable?.getRowModel().rows ?? []
|
|
757
|
-
const rowCount = rows.length ?? data.length
|
|
758
|
-
|
|
759
|
-
const nextRowIndex = rowIndex + 1
|
|
760
|
-
if (nextRowIndex < rowCount) {
|
|
761
|
-
requestAnimationFrame(() => {
|
|
762
|
-
focusCell(nextRowIndex, columnId)
|
|
763
|
-
})
|
|
764
|
-
}
|
|
765
|
-
} else if (opts?.direction && currentEditing) {
|
|
766
|
-
// Focus the current editing cell first, then navigate
|
|
767
|
-
const { rowIndex, columnId } = currentEditing
|
|
768
|
-
focusCell(rowIndex, columnId)
|
|
769
|
-
requestAnimationFrame(() => {
|
|
770
|
-
navigateCell(opts.direction ?? 'right')
|
|
771
|
-
})
|
|
772
|
-
}
|
|
773
|
-
},
|
|
774
|
-
[store, data.length, focusCell, navigateCell],
|
|
775
|
-
)
|
|
776
|
-
|
|
777
|
-
const onSearchOpenChange = React.useCallback(
|
|
778
|
-
(open: boolean) => {
|
|
779
|
-
if (open) {
|
|
780
|
-
store.setState('searchOpen', true)
|
|
781
|
-
return
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
const currentState = store.getState()
|
|
785
|
-
const currentMatch = currentState.matchIndex >= 0 && currentState.searchMatches[currentState.matchIndex]
|
|
786
|
-
|
|
787
|
-
store.batch(() => {
|
|
788
|
-
store.setState('searchOpen', false)
|
|
789
|
-
store.setState('searchQuery', '')
|
|
790
|
-
store.setState('searchMatches', [])
|
|
791
|
-
store.setState('matchIndex', -1)
|
|
792
|
-
|
|
793
|
-
if (currentMatch) {
|
|
794
|
-
store.setState('focusedCell', {
|
|
795
|
-
rowIndex: currentMatch.rowIndex,
|
|
796
|
-
columnId: currentMatch.columnId,
|
|
797
|
-
})
|
|
798
|
-
}
|
|
799
|
-
})
|
|
800
|
-
|
|
801
|
-
if (dataGridRef.current && document.activeElement !== dataGridRef.current) {
|
|
802
|
-
dataGridRef.current.focus()
|
|
803
|
-
}
|
|
804
|
-
},
|
|
805
|
-
[store],
|
|
806
|
-
)
|
|
807
|
-
|
|
808
|
-
const onSearch = React.useCallback(
|
|
809
|
-
(query: string) => {
|
|
810
|
-
if (!query.trim()) {
|
|
811
|
-
store.batch(() => {
|
|
812
|
-
store.setState('searchMatches', [])
|
|
813
|
-
store.setState('matchIndex', -1)
|
|
814
|
-
})
|
|
815
|
-
return
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
const matches: CellPosition[] = []
|
|
819
|
-
const currentTable = tableRef.current
|
|
820
|
-
const rows = currentTable?.getRowModel().rows ?? []
|
|
821
|
-
|
|
822
|
-
const lowerQuery = query.toLowerCase()
|
|
823
|
-
|
|
824
|
-
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
825
|
-
const row = rows[rowIndex]
|
|
826
|
-
if (!row) {
|
|
827
|
-
continue
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
for (const columnId of columnIds) {
|
|
831
|
-
const cell = row.getVisibleCells().find((c) => c.column.id === columnId)
|
|
832
|
-
if (!cell) {
|
|
833
|
-
continue
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
const value = cell.getValue()
|
|
837
|
-
const stringValue = String(value ?? '').toLowerCase()
|
|
838
|
-
|
|
839
|
-
if (stringValue.includes(lowerQuery)) {
|
|
840
|
-
matches.push({ rowIndex, columnId })
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
store.batch(() => {
|
|
846
|
-
store.setState('searchMatches', matches)
|
|
847
|
-
store.setState('matchIndex', matches.length > 0 ? 0 : -1)
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
// Scroll to first match but don't focus it (to keep focus in search input)
|
|
851
|
-
if (matches.length > 0 && matches[0]) {
|
|
852
|
-
const firstMatch = matches[0]
|
|
853
|
-
rowVirtualizerRef.current?.scrollToIndex(firstMatch.rowIndex, {
|
|
854
|
-
align: 'center',
|
|
855
|
-
})
|
|
856
|
-
}
|
|
857
|
-
},
|
|
858
|
-
[columnIds, store],
|
|
859
|
-
)
|
|
860
|
-
|
|
861
|
-
const onSearchQueryChange = React.useCallback((query: string) => store.setState('searchQuery', query), [store])
|
|
862
|
-
|
|
863
|
-
const onNavigateToPrevMatch = React.useCallback(() => {
|
|
864
|
-
const currentState = store.getState()
|
|
865
|
-
if (currentState.searchMatches.length === 0) {
|
|
866
|
-
return
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const prevIndex =
|
|
870
|
-
currentState.matchIndex - 1 < 0 ? currentState.searchMatches.length - 1 : currentState.matchIndex - 1
|
|
871
|
-
const match = currentState.searchMatches[prevIndex]
|
|
872
|
-
|
|
873
|
-
if (match) {
|
|
874
|
-
rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {
|
|
875
|
-
align: 'center',
|
|
876
|
-
})
|
|
877
|
-
|
|
878
|
-
requestAnimationFrame(() => {
|
|
879
|
-
store.setState('matchIndex', prevIndex)
|
|
880
|
-
requestAnimationFrame(() => {
|
|
881
|
-
focusCell(match.rowIndex, match.columnId)
|
|
882
|
-
})
|
|
883
|
-
})
|
|
884
|
-
}
|
|
885
|
-
}, [store, focusCell])
|
|
886
|
-
|
|
887
|
-
const onNavigateToNextMatch = React.useCallback(() => {
|
|
888
|
-
const currentState = store.getState()
|
|
889
|
-
if (currentState.searchMatches.length === 0) {
|
|
890
|
-
return
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const nextIndex = (currentState.matchIndex + 1) % currentState.searchMatches.length
|
|
894
|
-
const match = currentState.searchMatches[nextIndex]
|
|
895
|
-
|
|
896
|
-
if (match) {
|
|
897
|
-
rowVirtualizerRef.current?.scrollToIndex(match.rowIndex, {
|
|
898
|
-
align: 'center',
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
requestAnimationFrame(() => {
|
|
902
|
-
store.setState('matchIndex', nextIndex)
|
|
903
|
-
requestAnimationFrame(() => {
|
|
904
|
-
focusCell(match.rowIndex, match.columnId)
|
|
905
|
-
})
|
|
906
|
-
})
|
|
907
|
-
}
|
|
908
|
-
}, [store, focusCell])
|
|
909
|
-
|
|
910
|
-
const getIsSearchMatch = React.useCallback(
|
|
911
|
-
(rowIndex: number, columnId: string) => {
|
|
912
|
-
return searchMatches.some((match) => match.rowIndex === rowIndex && match.columnId === columnId)
|
|
913
|
-
},
|
|
914
|
-
[searchMatches],
|
|
915
|
-
)
|
|
916
|
-
|
|
917
|
-
const getIsActiveSearchMatch = React.useCallback(
|
|
918
|
-
(rowIndex: number, columnId: string) => {
|
|
919
|
-
if (matchIndex < 0) {
|
|
920
|
-
return false
|
|
921
|
-
}
|
|
922
|
-
const currentMatch = searchMatches[matchIndex]
|
|
923
|
-
return currentMatch?.rowIndex === rowIndex && currentMatch?.columnId === columnId
|
|
924
|
-
},
|
|
925
|
-
[searchMatches, matchIndex],
|
|
926
|
-
)
|
|
927
|
-
|
|
928
|
-
const blurCell = React.useCallback(() => {
|
|
929
|
-
const currentState = store.getState()
|
|
930
|
-
if (currentState.editingCell && document.activeElement instanceof HTMLElement) {
|
|
931
|
-
document.activeElement.blur()
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
store.batch(() => {
|
|
935
|
-
store.setState('focusedCell', null)
|
|
936
|
-
store.setState('editingCell', null)
|
|
937
|
-
})
|
|
938
|
-
}, [store])
|
|
939
|
-
|
|
940
|
-
const onCellClick = React.useCallback(
|
|
941
|
-
(rowIndex: number, columnId: string, event?: React.MouseEvent) => {
|
|
942
|
-
// Ignore right-click (button 2) - let onCellContextMenu handle it
|
|
943
|
-
if (event?.button === 2) {
|
|
944
|
-
return
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
const currentState = store.getState()
|
|
948
|
-
const currentFocused = currentState.focusedCell
|
|
949
|
-
|
|
950
|
-
if (event) {
|
|
951
|
-
if (event.ctrlKey || event.metaKey) {
|
|
952
|
-
event.preventDefault()
|
|
953
|
-
const cellKey = getCellKey(rowIndex, columnId)
|
|
954
|
-
const newSelectedCells = new Set(currentState.selectionState.selectedCells)
|
|
955
|
-
|
|
956
|
-
if (newSelectedCells.has(cellKey)) {
|
|
957
|
-
newSelectedCells.delete(cellKey)
|
|
958
|
-
} else {
|
|
959
|
-
newSelectedCells.add(cellKey)
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
store.setState('selectionState', {
|
|
963
|
-
selectedCells: newSelectedCells,
|
|
964
|
-
selectionRange: null,
|
|
965
|
-
isSelecting: false,
|
|
966
|
-
})
|
|
967
|
-
focusCell(rowIndex, columnId)
|
|
968
|
-
return
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
if (event.shiftKey && currentState.focusedCell) {
|
|
972
|
-
event.preventDefault()
|
|
973
|
-
selectRange(currentState.focusedCell, { rowIndex, columnId })
|
|
974
|
-
return
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
|
|
978
|
-
// Clear selection if there are selected cells or rows
|
|
979
|
-
const hasSelectedCells = currentState.selectionState.selectedCells.size > 0
|
|
980
|
-
const hasSelectedRows = Object.keys(currentState.rowSelection).length > 0
|
|
981
|
-
|
|
982
|
-
if (hasSelectedCells && !currentState.selectionState.isSelecting) {
|
|
983
|
-
// If there's a cell selection but we're not actively selecting (drag just finished),
|
|
984
|
-
// don't clear it - keep the selection
|
|
985
|
-
// Only clear if clicking elsewhere
|
|
986
|
-
const cellKey = getCellKey(rowIndex, columnId)
|
|
987
|
-
const isClickingSelectedCell = currentState.selectionState.selectedCells.has(cellKey)
|
|
988
|
-
|
|
989
|
-
if (!isClickingSelectedCell) {
|
|
990
|
-
clearSelection()
|
|
991
|
-
} else {
|
|
992
|
-
// Clicking on an already selected cell - just focus it
|
|
993
|
-
focusCell(rowIndex, columnId)
|
|
994
|
-
return
|
|
995
|
-
}
|
|
996
|
-
} else if (hasSelectedRows && columnId !== 'select') {
|
|
997
|
-
// If there are selected rows but we're clicking on a non-checkbox cell, clear selections
|
|
998
|
-
clearSelection()
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
if (currentFocused?.rowIndex === rowIndex && currentFocused?.columnId === columnId) {
|
|
1002
|
-
onCellEditingStart(rowIndex, columnId)
|
|
1003
|
-
} else {
|
|
1004
|
-
focusCell(rowIndex, columnId)
|
|
1005
|
-
}
|
|
1006
|
-
},
|
|
1007
|
-
[store, focusCell, onCellEditingStart, selectRange, clearSelection],
|
|
1008
|
-
)
|
|
1009
|
-
|
|
1010
|
-
const onCellDoubleClick = React.useCallback(
|
|
1011
|
-
(rowIndex: number, columnId: string, event?: React.MouseEvent) => {
|
|
1012
|
-
if (event?.defaultPrevented) {
|
|
1013
|
-
return
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
onCellEditingStart(rowIndex, columnId)
|
|
1017
|
-
},
|
|
1018
|
-
[onCellEditingStart],
|
|
1019
|
-
)
|
|
1020
|
-
|
|
1021
|
-
const onCellMouseDown = React.useCallback(
|
|
1022
|
-
(rowIndex: number, columnId: string, event: React.MouseEvent) => {
|
|
1023
|
-
// Ignore right-click (button 2) - let onCellContextMenu handle it
|
|
1024
|
-
if (event.button === 2) {
|
|
1025
|
-
return
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
event.preventDefault()
|
|
1029
|
-
|
|
1030
|
-
// Only start drag selection if no modifier keys are pressed
|
|
1031
|
-
// Clear any existing selection and prepare for potential drag
|
|
1032
|
-
if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {
|
|
1033
|
-
store.batch(() => {
|
|
1034
|
-
store.setState('selectionState', {
|
|
1035
|
-
selectedCells: new Set(),
|
|
1036
|
-
selectionRange: {
|
|
1037
|
-
start: { rowIndex, columnId },
|
|
1038
|
-
end: { rowIndex, columnId },
|
|
1039
|
-
},
|
|
1040
|
-
isSelecting: true,
|
|
1041
|
-
})
|
|
1042
|
-
store.setState('rowSelection', {})
|
|
1043
|
-
})
|
|
1044
|
-
}
|
|
1045
|
-
},
|
|
1046
|
-
[store],
|
|
1047
|
-
)
|
|
1048
|
-
|
|
1049
|
-
const onCellMouseEnter = React.useCallback(
|
|
1050
|
-
(rowIndex: number, columnId: string, _event: React.MouseEvent) => {
|
|
1051
|
-
const currentState = store.getState()
|
|
1052
|
-
if (currentState.selectionState.isSelecting && currentState.selectionState.selectionRange) {
|
|
1053
|
-
const start = currentState.selectionState.selectionRange.start
|
|
1054
|
-
const end = { rowIndex, columnId }
|
|
1055
|
-
|
|
1056
|
-
if (
|
|
1057
|
-
currentState.focusedCell?.rowIndex !== start.rowIndex ||
|
|
1058
|
-
currentState.focusedCell?.columnId !== start.columnId
|
|
1059
|
-
) {
|
|
1060
|
-
focusCell(start.rowIndex, start.columnId)
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
selectRange(start, end, true)
|
|
1064
|
-
}
|
|
1065
|
-
},
|
|
1066
|
-
[store, selectRange, focusCell],
|
|
1067
|
-
)
|
|
1068
|
-
|
|
1069
|
-
const onCellMouseUp = React.useCallback(() => {
|
|
1070
|
-
const currentState = store.getState()
|
|
1071
|
-
store.setState('selectionState', {
|
|
1072
|
-
...currentState.selectionState,
|
|
1073
|
-
isSelecting: false,
|
|
1074
|
-
})
|
|
1075
|
-
}, [store])
|
|
1076
|
-
|
|
1077
|
-
const onCellContextMenu = React.useCallback(
|
|
1078
|
-
(rowIndex: number, columnId: string, event: React.MouseEvent) => {
|
|
1079
|
-
event.preventDefault()
|
|
1080
|
-
event.stopPropagation()
|
|
1081
|
-
|
|
1082
|
-
const currentState = store.getState()
|
|
1083
|
-
const cellKey = getCellKey(rowIndex, columnId)
|
|
1084
|
-
const isTargetCellSelected = currentState.selectionState.selectedCells.has(cellKey)
|
|
1085
|
-
|
|
1086
|
-
// If right-clicking on a non-selected cell, select only that cell
|
|
1087
|
-
if (!isTargetCellSelected) {
|
|
1088
|
-
store.batch(() => {
|
|
1089
|
-
store.setState('selectionState', {
|
|
1090
|
-
selectedCells: new Set([cellKey]),
|
|
1091
|
-
selectionRange: {
|
|
1092
|
-
start: { rowIndex, columnId },
|
|
1093
|
-
end: { rowIndex, columnId },
|
|
1094
|
-
},
|
|
1095
|
-
isSelecting: false,
|
|
1096
|
-
})
|
|
1097
|
-
store.setState('focusedCell', { rowIndex, columnId })
|
|
1098
|
-
})
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
// Open context menu at cursor position
|
|
1102
|
-
store.setState('contextMenu', {
|
|
1103
|
-
open: true,
|
|
1104
|
-
x: event.clientX,
|
|
1105
|
-
y: event.clientY,
|
|
1106
|
-
})
|
|
1107
|
-
},
|
|
1108
|
-
[store],
|
|
1109
|
-
)
|
|
1110
|
-
|
|
1111
|
-
const onContextMenuOpenChange = React.useCallback(
|
|
1112
|
-
(open: boolean) => {
|
|
1113
|
-
if (!open) {
|
|
1114
|
-
const currentMenu = store.getState().contextMenu
|
|
1115
|
-
store.setState('contextMenu', {
|
|
1116
|
-
open: false,
|
|
1117
|
-
x: currentMenu.x,
|
|
1118
|
-
y: currentMenu.y,
|
|
1119
|
-
})
|
|
1120
|
-
}
|
|
1121
|
-
},
|
|
1122
|
-
[store],
|
|
1123
|
-
)
|
|
1124
|
-
|
|
1125
|
-
const onDataGridKeyDown = React.useCallback(
|
|
1126
|
-
(event: KeyboardEvent) => {
|
|
1127
|
-
const currentState = store.getState()
|
|
1128
|
-
const { key, ctrlKey, metaKey, shiftKey } = event
|
|
1129
|
-
const isCtrlPressed = ctrlKey || metaKey
|
|
1130
|
-
|
|
1131
|
-
// Handle Cmd+F / Ctrl+F to open search (highest priority, works even when editing)
|
|
1132
|
-
if (enableSearch && isCtrlPressed && key === SEARCH_SHORTCUT_KEY) {
|
|
1133
|
-
event.preventDefault()
|
|
1134
|
-
onSearchOpenChange(true)
|
|
1135
|
-
return
|
|
1136
|
-
}
|
|
1137
|
-
|
|
1138
|
-
// Handle search navigation when search is open
|
|
1139
|
-
if (enableSearch && currentState.searchOpen && !currentState.editingCell) {
|
|
1140
|
-
if (key === 'Enter') {
|
|
1141
|
-
event.preventDefault()
|
|
1142
|
-
if (shiftKey) {
|
|
1143
|
-
onNavigateToPrevMatch()
|
|
1144
|
-
} else {
|
|
1145
|
-
onNavigateToNextMatch()
|
|
1146
|
-
}
|
|
1147
|
-
return
|
|
1148
|
-
}
|
|
1149
|
-
if (key === 'Escape') {
|
|
1150
|
-
event.preventDefault()
|
|
1151
|
-
onSearchOpenChange(false)
|
|
1152
|
-
return
|
|
1153
|
-
}
|
|
1154
|
-
// When search is open, don't let data grid handle any other keys
|
|
1155
|
-
// (they should only affect the search input)
|
|
1156
|
-
return
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
if (currentState.editingCell) {
|
|
1160
|
-
return
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
if (!currentState.focusedCell) {
|
|
1164
|
-
return
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
let direction: NavigationDirection | null = null
|
|
1168
|
-
|
|
1169
|
-
if (isCtrlPressed && key === 'a') {
|
|
1170
|
-
event.preventDefault()
|
|
1171
|
-
selectAll()
|
|
1172
|
-
return
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
if (key === 'Delete' || key === 'Backspace') {
|
|
1176
|
-
if (currentState.selectionState.selectedCells.size > 0) {
|
|
1177
|
-
event.preventDefault()
|
|
1178
|
-
// If any selected cell is not editable, do nothing (mirror context-menu Clear behavior)
|
|
1179
|
-
const currentTable = tableRef.current
|
|
1180
|
-
const visibleCols = currentTable?.getVisibleLeafColumns() ?? []
|
|
1181
|
-
let canClear = true
|
|
1182
|
-
for (const cellKey of currentState.selectionState.selectedCells) {
|
|
1183
|
-
if (!canClear) {
|
|
1184
|
-
break
|
|
1185
|
-
}
|
|
1186
|
-
const { columnId } = parseCellKey(cellKey)
|
|
1187
|
-
const col = visibleCols.find((c) => c.id === columnId)
|
|
1188
|
-
const editable = col?.columnDef?.meta?.editable
|
|
1189
|
-
if (editable === false) {
|
|
1190
|
-
canClear = false
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
if (!canClear) {
|
|
1195
|
-
return
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
const updates: {
|
|
1199
|
-
rowIndex: number
|
|
1200
|
-
columnId: string
|
|
1201
|
-
value: unknown
|
|
1202
|
-
}[] = []
|
|
1203
|
-
|
|
1204
|
-
currentState.selectionState.selectedCells.forEach((cellKey) => {
|
|
1205
|
-
const { rowIndex, columnId } = parseCellKey(cellKey)
|
|
1206
|
-
updates.push({ rowIndex, columnId, value: '' })
|
|
1207
|
-
})
|
|
1208
|
-
|
|
1209
|
-
onDataUpdate(updates)
|
|
1210
|
-
clearSelection()
|
|
1211
|
-
}
|
|
1212
|
-
return
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
switch (key) {
|
|
1216
|
-
case 'ArrowUp':
|
|
1217
|
-
direction = 'up'
|
|
1218
|
-
break
|
|
1219
|
-
case 'ArrowDown':
|
|
1220
|
-
direction = 'down'
|
|
1221
|
-
break
|
|
1222
|
-
case 'ArrowLeft':
|
|
1223
|
-
direction = 'left'
|
|
1224
|
-
break
|
|
1225
|
-
case 'ArrowRight':
|
|
1226
|
-
direction = 'right'
|
|
1227
|
-
break
|
|
1228
|
-
case 'Home':
|
|
1229
|
-
direction = isCtrlPressed ? 'ctrl+home' : 'home'
|
|
1230
|
-
break
|
|
1231
|
-
case 'End':
|
|
1232
|
-
direction = isCtrlPressed ? 'ctrl+end' : 'end'
|
|
1233
|
-
break
|
|
1234
|
-
case 'PageUp':
|
|
1235
|
-
direction = 'pageup'
|
|
1236
|
-
break
|
|
1237
|
-
case 'PageDown':
|
|
1238
|
-
direction = 'pagedown'
|
|
1239
|
-
break
|
|
1240
|
-
case 'Escape':
|
|
1241
|
-
event.preventDefault()
|
|
1242
|
-
if (currentState.selectionState.selectedCells.size > 0 || Object.keys(currentState.rowSelection).length > 0) {
|
|
1243
|
-
clearSelection()
|
|
1244
|
-
} else {
|
|
1245
|
-
blurCell()
|
|
1246
|
-
}
|
|
1247
|
-
return
|
|
1248
|
-
case 'Tab':
|
|
1249
|
-
event.preventDefault()
|
|
1250
|
-
direction = event.shiftKey ? 'left' : 'right'
|
|
1251
|
-
break
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
if (direction) {
|
|
1255
|
-
event.preventDefault()
|
|
1256
|
-
|
|
1257
|
-
// Tab navigation should not trigger selection, even with Shift
|
|
1258
|
-
if (shiftKey && key !== 'Tab' && currentState.focusedCell) {
|
|
1259
|
-
const _navCols = getNavigableColumnIds()
|
|
1260
|
-
const currentColIndex = _navCols.indexOf(currentState.focusedCell.columnId)
|
|
1261
|
-
let newRowIndex = currentState.focusedCell.rowIndex
|
|
1262
|
-
let newColumnId = currentState.focusedCell.columnId
|
|
1263
|
-
|
|
1264
|
-
switch (direction) {
|
|
1265
|
-
case 'up':
|
|
1266
|
-
newRowIndex = Math.max(0, currentState.focusedCell.rowIndex - 1)
|
|
1267
|
-
break
|
|
1268
|
-
case 'down':
|
|
1269
|
-
newRowIndex = Math.min(
|
|
1270
|
-
(tableRef.current?.getRowModel().rows.length || data.length) - 1,
|
|
1271
|
-
currentState.focusedCell.rowIndex + 1,
|
|
1272
|
-
)
|
|
1273
|
-
break
|
|
1274
|
-
case 'left':
|
|
1275
|
-
if (currentColIndex > 0) {
|
|
1276
|
-
const prevColumnId = _navCols[currentColIndex - 1]
|
|
1277
|
-
if (prevColumnId) {
|
|
1278
|
-
newColumnId = prevColumnId
|
|
1279
|
-
}
|
|
1280
|
-
}
|
|
1281
|
-
break
|
|
1282
|
-
case 'right':
|
|
1283
|
-
if (currentColIndex < _navCols.length - 1) {
|
|
1284
|
-
const nextColumnId = _navCols[currentColIndex + 1]
|
|
1285
|
-
if (nextColumnId) {
|
|
1286
|
-
newColumnId = nextColumnId
|
|
1287
|
-
}
|
|
1288
|
-
}
|
|
1289
|
-
break
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
const selectionStart = currentState.selectionState.selectionRange?.start || currentState.focusedCell
|
|
1293
|
-
selectRange(selectionStart, {
|
|
1294
|
-
rowIndex: newRowIndex,
|
|
1295
|
-
columnId: newColumnId,
|
|
1296
|
-
})
|
|
1297
|
-
focusCell(newRowIndex, newColumnId)
|
|
1298
|
-
} else {
|
|
1299
|
-
if (currentState.selectionState.selectedCells.size > 0) {
|
|
1300
|
-
clearSelection()
|
|
1301
|
-
}
|
|
1302
|
-
navigateCell(direction)
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
},
|
|
1306
|
-
[
|
|
1307
|
-
store,
|
|
1308
|
-
blurCell,
|
|
1309
|
-
navigateCell,
|
|
1310
|
-
selectAll,
|
|
1311
|
-
onDataUpdate,
|
|
1312
|
-
clearSelection,
|
|
1313
|
-
getNavigableColumnIds,
|
|
1314
|
-
data.length,
|
|
1315
|
-
selectRange,
|
|
1316
|
-
focusCell,
|
|
1317
|
-
onSearchOpenChange,
|
|
1318
|
-
onNavigateToNextMatch,
|
|
1319
|
-
onNavigateToPrevMatch,
|
|
1320
|
-
enableSearch,
|
|
1321
|
-
],
|
|
1322
|
-
)
|
|
1323
|
-
|
|
1324
|
-
const onSortingChange = React.useCallback(
|
|
1325
|
-
(updater: Updater<SortingState>) => {
|
|
1326
|
-
const currentState = store.getState()
|
|
1327
|
-
const newSorting = typeof updater === 'function' ? updater(currentState.sorting) : updater
|
|
1328
|
-
store.setState('sorting', newSorting)
|
|
1329
|
-
},
|
|
1330
|
-
[store],
|
|
1331
|
-
)
|
|
1332
|
-
|
|
1333
|
-
const onRowSelectionChange = React.useCallback(
|
|
1334
|
-
(updater: Updater<RowSelectionState>) => {
|
|
1335
|
-
const currentState = store.getState()
|
|
1336
|
-
const newRowSelection = typeof updater === 'function' ? updater(currentState.rowSelection) : updater
|
|
1337
|
-
|
|
1338
|
-
const selectedRows = Object.keys(newRowSelection).filter((key) => newRowSelection[key])
|
|
1339
|
-
|
|
1340
|
-
const selectedCells = new Set<string>()
|
|
1341
|
-
const rows = tableRef.current?.getRowModel().rows ?? []
|
|
1342
|
-
|
|
1343
|
-
for (const rowId of selectedRows) {
|
|
1344
|
-
const rowIndex = rows.findIndex((r) => r.id === rowId)
|
|
1345
|
-
if (rowIndex === -1) {
|
|
1346
|
-
continue
|
|
1347
|
-
}
|
|
1348
|
-
|
|
1349
|
-
for (const columnId of columnIds) {
|
|
1350
|
-
selectedCells.add(getCellKey(rowIndex, columnId))
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
store.batch(() => {
|
|
1355
|
-
store.setState('rowSelection', newRowSelection)
|
|
1356
|
-
store.setState('selectionState', {
|
|
1357
|
-
selectedCells,
|
|
1358
|
-
selectionRange: null,
|
|
1359
|
-
isSelecting: false,
|
|
1360
|
-
})
|
|
1361
|
-
store.setState('focusedCell', null)
|
|
1362
|
-
store.setState('editingCell', null)
|
|
1363
|
-
})
|
|
1364
|
-
},
|
|
1365
|
-
[store, columnIds],
|
|
1366
|
-
)
|
|
1367
|
-
|
|
1368
|
-
const onRowSelect = React.useCallback(
|
|
1369
|
-
(rowIndex: number, selected: boolean, shiftKey: boolean) => {
|
|
1370
|
-
const currentState = store.getState()
|
|
1371
|
-
const rows = tableRef.current?.getRowModel().rows ?? []
|
|
1372
|
-
const currentRow = rows[rowIndex]
|
|
1373
|
-
if (!currentRow) {
|
|
1374
|
-
return
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
if (shiftKey && currentState.lastClickedRowIndex !== null) {
|
|
1378
|
-
const startIndex = Math.min(currentState.lastClickedRowIndex, rowIndex)
|
|
1379
|
-
const endIndex = Math.max(currentState.lastClickedRowIndex, rowIndex)
|
|
1380
|
-
|
|
1381
|
-
const newRowSelection: RowSelectionState = {
|
|
1382
|
-
...currentState.rowSelection,
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
for (let i = startIndex; i <= endIndex; i++) {
|
|
1386
|
-
const row = rows[i]
|
|
1387
|
-
if (row) {
|
|
1388
|
-
newRowSelection[row.id] = selected
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
onRowSelectionChange(newRowSelection)
|
|
1393
|
-
} else {
|
|
1394
|
-
onRowSelectionChange({
|
|
1395
|
-
...currentState.rowSelection,
|
|
1396
|
-
[currentRow.id]: selected,
|
|
1397
|
-
})
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
store.setState('lastClickedRowIndex', rowIndex)
|
|
1401
|
-
},
|
|
1402
|
-
[store, onRowSelectionChange],
|
|
1403
|
-
)
|
|
1404
|
-
|
|
1405
|
-
const onRowHeightChange = React.useCallback(
|
|
1406
|
-
(updater: Updater<RowHeightValue>) => {
|
|
1407
|
-
const currentState = store.getState()
|
|
1408
|
-
const newRowHeight = typeof updater === 'function' ? updater(currentState.rowHeight) : updater
|
|
1409
|
-
store.setState('rowHeight', newRowHeight)
|
|
1410
|
-
},
|
|
1411
|
-
[store],
|
|
1412
|
-
)
|
|
1413
|
-
|
|
1414
|
-
const onColumnClick = React.useCallback(
|
|
1415
|
-
(columnId: string) => {
|
|
1416
|
-
if (!enableColumnSelection) {
|
|
1417
|
-
clearSelection()
|
|
1418
|
-
return
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
selectColumn(columnId)
|
|
1422
|
-
},
|
|
1423
|
-
[enableColumnSelection, selectColumn, clearSelection],
|
|
1424
|
-
)
|
|
1425
|
-
|
|
1426
|
-
const defaultColumn: Partial<ColumnDef<TData>> = React.useMemo(
|
|
1427
|
-
() => ({
|
|
1428
|
-
cell: DataGridCell,
|
|
1429
|
-
minSize: MIN_COLUMN_SIZE,
|
|
1430
|
-
maxSize: MAX_COLUMN_SIZE,
|
|
1431
|
-
}),
|
|
1432
|
-
[],
|
|
1433
|
-
)
|
|
1434
|
-
|
|
1435
|
-
const tableOptions = React.useMemo<TableOptions<TData>>(
|
|
1436
|
-
() => ({
|
|
1437
|
-
...dataGridPropsRef.current,
|
|
1438
|
-
data,
|
|
1439
|
-
columns,
|
|
1440
|
-
defaultColumn,
|
|
1441
|
-
initialState: mergedInitialState,
|
|
1442
|
-
state: {
|
|
1443
|
-
...dataGridPropsRef.current.state,
|
|
1444
|
-
sorting,
|
|
1445
|
-
rowSelection,
|
|
1446
|
-
},
|
|
1447
|
-
onRowSelectionChange,
|
|
1448
|
-
onSortingChange,
|
|
1449
|
-
columnResizeMode: 'onChange',
|
|
1450
|
-
getCoreRowModel: getCoreRowModel(),
|
|
1451
|
-
getSortedRowModel: getSortedRowModel(),
|
|
1452
|
-
meta: {
|
|
1453
|
-
...dataGridPropsRef.current.meta,
|
|
1454
|
-
dataGridRef,
|
|
1455
|
-
focusedCell,
|
|
1456
|
-
editingCell,
|
|
1457
|
-
selectionState,
|
|
1458
|
-
searchOpen,
|
|
1459
|
-
rowHeight,
|
|
1460
|
-
isScrolling,
|
|
1461
|
-
getIsCellSelected,
|
|
1462
|
-
getIsSearchMatch,
|
|
1463
|
-
getIsActiveSearchMatch,
|
|
1464
|
-
onRowHeightChange,
|
|
1465
|
-
onRowSelect,
|
|
1466
|
-
onRowsDelete: onRowsDeleteProp ? onRowsDelete : undefined,
|
|
1467
|
-
onDataUpdate,
|
|
1468
|
-
onColumnClick,
|
|
1469
|
-
onCellClick,
|
|
1470
|
-
onCellDoubleClick,
|
|
1471
|
-
onCellMouseDown,
|
|
1472
|
-
onCellMouseEnter,
|
|
1473
|
-
onCellMouseUp,
|
|
1474
|
-
onCellContextMenu,
|
|
1475
|
-
onCellEditingStart,
|
|
1476
|
-
onCellEditingStop,
|
|
1477
|
-
contextMenu,
|
|
1478
|
-
onContextMenuOpenChange,
|
|
1479
|
-
},
|
|
1480
|
-
}),
|
|
1481
|
-
[
|
|
1482
|
-
dataGridPropsRef,
|
|
1483
|
-
data,
|
|
1484
|
-
columns,
|
|
1485
|
-
defaultColumn,
|
|
1486
|
-
mergedInitialState,
|
|
1487
|
-
sorting,
|
|
1488
|
-
rowSelection,
|
|
1489
|
-
onRowSelectionChange,
|
|
1490
|
-
onSortingChange,
|
|
1491
|
-
focusedCell,
|
|
1492
|
-
editingCell,
|
|
1493
|
-
selectionState,
|
|
1494
|
-
searchOpen,
|
|
1495
|
-
isScrolling,
|
|
1496
|
-
getIsCellSelected,
|
|
1497
|
-
getIsSearchMatch,
|
|
1498
|
-
getIsActiveSearchMatch,
|
|
1499
|
-
onDataUpdate,
|
|
1500
|
-
onRowsDeleteProp,
|
|
1501
|
-
onRowsDelete,
|
|
1502
|
-
onColumnClick,
|
|
1503
|
-
onCellClick,
|
|
1504
|
-
onCellDoubleClick,
|
|
1505
|
-
onCellMouseDown,
|
|
1506
|
-
onCellMouseEnter,
|
|
1507
|
-
onCellMouseUp,
|
|
1508
|
-
onCellContextMenu,
|
|
1509
|
-
onCellEditingStart,
|
|
1510
|
-
onCellEditingStop,
|
|
1511
|
-
contextMenu,
|
|
1512
|
-
onContextMenuOpenChange,
|
|
1513
|
-
rowHeight,
|
|
1514
|
-
onRowHeightChange,
|
|
1515
|
-
onRowSelect,
|
|
1516
|
-
],
|
|
1517
|
-
)
|
|
1518
|
-
|
|
1519
|
-
const table = useReactTable(tableOptions)
|
|
1520
|
-
|
|
1521
|
-
if (!tableRef.current) {
|
|
1522
|
-
tableRef.current = table
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
// Extract columnSizing once so it can be used in hook dependency arrays
|
|
1526
|
-
const columnSizing = table.getState().columnSizing
|
|
1527
|
-
|
|
1528
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: we need to memoize the column size vars
|
|
1529
|
-
const columnSizeVars = React.useMemo(() => {
|
|
1530
|
-
// reference columnSizing so it is a used dependency (recomputes when column sizing changes)
|
|
1531
|
-
void columnSizing
|
|
1532
|
-
const headers = table.getFlatHeaders()
|
|
1533
|
-
const colSizes: Record<string, number> = {}
|
|
1534
|
-
for (const header of headers) {
|
|
1535
|
-
// Prefer the current header size, but fall back to the columnDef `size`
|
|
1536
|
-
// or the MIN_COLUMN_SIZE so the CSS variables exist on first render.
|
|
1537
|
-
const headerSize =
|
|
1538
|
-
(typeof header.getSize === 'function' ? header.getSize() : undefined) ??
|
|
1539
|
-
(header.column.columnDef as any)?.size ??
|
|
1540
|
-
MIN_COLUMN_SIZE
|
|
1541
|
-
const colSize =
|
|
1542
|
-
(typeof header.column.getSize === 'function' ? header.column.getSize() : undefined) ??
|
|
1543
|
-
(header.column.columnDef as any)?.size ??
|
|
1544
|
-
MIN_COLUMN_SIZE
|
|
1545
|
-
|
|
1546
|
-
colSizes[`--header-${header.id}-size`] = headerSize
|
|
1547
|
-
colSizes[`--col-${header.column.id}-size`] = colSize
|
|
1548
|
-
}
|
|
1549
|
-
return colSizes
|
|
1550
|
-
// Recompute whenever the table's column sizing state changes so CSS vars reflect new sizes
|
|
1551
|
-
}, [table, columnSizing])
|
|
1552
|
-
|
|
1553
|
-
const rowVirtualizer = useVirtualizer({
|
|
1554
|
-
count: table.getRowModel().rows.length,
|
|
1555
|
-
getScrollElement: () => dataGridRef.current,
|
|
1556
|
-
estimateSize: () => rowHeightValue,
|
|
1557
|
-
overscan,
|
|
1558
|
-
measureElement:
|
|
1559
|
-
typeof window !== 'undefined' && navigator.userAgent.indexOf('Firefox') === -1
|
|
1560
|
-
? (element) => element?.getBoundingClientRect().height
|
|
1561
|
-
: undefined,
|
|
1562
|
-
onChange: (instance) => {
|
|
1563
|
-
// Sync virtualizer's isScrolling state to our store
|
|
1564
|
-
const virtualizerIsScrolling = instance.isScrolling
|
|
1565
|
-
const currentIsScrolling = store.getState().isScrolling
|
|
1566
|
-
|
|
1567
|
-
if (virtualizerIsScrolling !== currentIsScrolling) {
|
|
1568
|
-
store.setState('isScrolling', virtualizerIsScrolling)
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
// Batch DOM updates in a single animation frame
|
|
1572
|
-
const virtualItems = instance.getVirtualItems()
|
|
1573
|
-
if (virtualItems.length === 0) {
|
|
1574
|
-
return
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
requestAnimationFrame(() => {
|
|
1578
|
-
for (const virtualRow of virtualItems) {
|
|
1579
|
-
if (!virtualRow) {
|
|
1580
|
-
continue
|
|
1581
|
-
}
|
|
1582
|
-
const rowRef = rowMapRef.current.get(virtualRow.index)
|
|
1583
|
-
if (rowRef) {
|
|
1584
|
-
rowRef.style.transform = `translateY(${virtualRow.start}px)`
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
})
|
|
1588
|
-
},
|
|
1589
|
-
})
|
|
1590
|
-
|
|
1591
|
-
if (!rowVirtualizerRef.current) {
|
|
1592
|
-
rowVirtualizerRef.current = rowVirtualizer
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
const onScrollToRow = React.useCallback(
|
|
1596
|
-
(opts: Partial<CellPosition>) => {
|
|
1597
|
-
const rowIndex = opts?.rowIndex ?? 0
|
|
1598
|
-
const columnId = opts?.columnId
|
|
1599
|
-
|
|
1600
|
-
rowVirtualizer.scrollToIndex(rowIndex, {
|
|
1601
|
-
align: 'center',
|
|
1602
|
-
})
|
|
1603
|
-
|
|
1604
|
-
const targetColumnId = columnId ?? getNavigableColumnIds()[0]
|
|
1605
|
-
|
|
1606
|
-
if (!targetColumnId) {
|
|
1607
|
-
return
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
queueMicrotask(() => {
|
|
1611
|
-
requestAnimationFrame(() => {
|
|
1612
|
-
requestAnimationFrame(() => {
|
|
1613
|
-
store.batch(() => {
|
|
1614
|
-
store.setState('focusedCell', {
|
|
1615
|
-
rowIndex,
|
|
1616
|
-
columnId: targetColumnId,
|
|
1617
|
-
})
|
|
1618
|
-
store.setState('editingCell', null)
|
|
1619
|
-
})
|
|
1620
|
-
})
|
|
1621
|
-
})
|
|
1622
|
-
})
|
|
1623
|
-
},
|
|
1624
|
-
[rowVirtualizer, getNavigableColumnIds, store],
|
|
1625
|
-
)
|
|
1626
|
-
|
|
1627
|
-
const onRowAdd = React.useCallback(
|
|
1628
|
-
async (event?: React.MouseEvent<HTMLDivElement>) => {
|
|
1629
|
-
if (!onRowAddProp) {
|
|
1630
|
-
return
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
const result = await onRowAddProp(event)
|
|
1634
|
-
|
|
1635
|
-
if (event?.defaultPrevented || result === null) {
|
|
1636
|
-
return
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
const currentTable = tableRef.current
|
|
1640
|
-
const rows = currentTable?.getRowModel().rows ?? []
|
|
1641
|
-
|
|
1642
|
-
if (result) {
|
|
1643
|
-
const adjustedRowIndex = (result.rowIndex ?? 0) >= rows.length ? rows.length : result.rowIndex
|
|
1644
|
-
|
|
1645
|
-
onScrollToRow({
|
|
1646
|
-
rowIndex: adjustedRowIndex,
|
|
1647
|
-
columnId: result.columnId,
|
|
1648
|
-
})
|
|
1649
|
-
return
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
onScrollToRow({ rowIndex: rows.length })
|
|
1653
|
-
},
|
|
1654
|
-
[onRowAddProp, onScrollToRow],
|
|
1655
|
-
)
|
|
1656
|
-
|
|
1657
|
-
// Persist table column state (order, visibility, pinning) whenever it changes.
|
|
1658
|
-
// Use a layout effect so persistence stays in sync with table layout updates.
|
|
1659
|
-
useIsomorphicLayoutEffect(() => {
|
|
1660
|
-
if (!storageKey) {
|
|
1661
|
-
return
|
|
1662
|
-
}
|
|
1663
|
-
|
|
1664
|
-
try {
|
|
1665
|
-
const state = table.getState()
|
|
1666
|
-
const payload = {
|
|
1667
|
-
columnOrder: state.columnOrder,
|
|
1668
|
-
columnVisibility: state.columnVisibility,
|
|
1669
|
-
columnPinning: state.columnPinning,
|
|
1670
|
-
}
|
|
1671
|
-
localStorage.setItem(storageKey, JSON.stringify(payload))
|
|
1672
|
-
} catch (_) {
|
|
1673
|
-
// ignore storage errors
|
|
1674
|
-
}
|
|
1675
|
-
}, [storageKey, table.getState().columnOrder, table.getState().columnVisibility, table.getState().columnPinning])
|
|
1676
|
-
|
|
1677
|
-
const searchState = React.useMemo<SearchState | undefined>(() => {
|
|
1678
|
-
if (!enableSearch) {
|
|
1679
|
-
return undefined
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
return {
|
|
1683
|
-
searchMatches,
|
|
1684
|
-
matchIndex,
|
|
1685
|
-
searchOpen,
|
|
1686
|
-
onSearchOpenChange,
|
|
1687
|
-
searchQuery,
|
|
1688
|
-
onSearchQueryChange,
|
|
1689
|
-
onSearch,
|
|
1690
|
-
onNavigateToNextMatch,
|
|
1691
|
-
onNavigateToPrevMatch,
|
|
1692
|
-
}
|
|
1693
|
-
}, [
|
|
1694
|
-
enableSearch,
|
|
1695
|
-
searchMatches,
|
|
1696
|
-
matchIndex,
|
|
1697
|
-
searchOpen,
|
|
1698
|
-
onSearchOpenChange,
|
|
1699
|
-
searchQuery,
|
|
1700
|
-
onSearchQueryChange,
|
|
1701
|
-
onSearch,
|
|
1702
|
-
onNavigateToNextMatch,
|
|
1703
|
-
onNavigateToPrevMatch,
|
|
1704
|
-
])
|
|
1705
|
-
|
|
1706
|
-
React.useEffect(() => {
|
|
1707
|
-
const dataGridElement = dataGridRef.current
|
|
1708
|
-
if (!dataGridElement) {
|
|
1709
|
-
return
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
dataGridElement.addEventListener('keydown', onDataGridKeyDown)
|
|
1713
|
-
return () => {
|
|
1714
|
-
dataGridElement.removeEventListener('keydown', onDataGridKeyDown)
|
|
1715
|
-
}
|
|
1716
|
-
}, [onDataGridKeyDown])
|
|
1717
|
-
|
|
1718
|
-
React.useEffect(() => {
|
|
1719
|
-
function onGlobalKeyDown(event: KeyboardEvent) {
|
|
1720
|
-
const dataGridElement = dataGridRef.current
|
|
1721
|
-
if (!dataGridElement) {
|
|
1722
|
-
return
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
const target = event.target
|
|
1726
|
-
if (!(target instanceof HTMLElement)) {
|
|
1727
|
-
return
|
|
1728
|
-
}
|
|
1729
|
-
|
|
1730
|
-
const { key, ctrlKey, metaKey } = event
|
|
1731
|
-
const isCtrlPressed = ctrlKey || metaKey
|
|
1732
|
-
|
|
1733
|
-
if (enableSearch && isCtrlPressed && key === SEARCH_SHORTCUT_KEY) {
|
|
1734
|
-
const isInInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA'
|
|
1735
|
-
const isInDataGrid = dataGridElement.contains(target)
|
|
1736
|
-
const isInSearchInput = target.closest('[role=search]') !== null
|
|
1737
|
-
|
|
1738
|
-
if (isInDataGrid || isInSearchInput || !isInInput) {
|
|
1739
|
-
event.preventDefault()
|
|
1740
|
-
event.stopPropagation()
|
|
1741
|
-
onSearchOpenChange(true)
|
|
1742
|
-
|
|
1743
|
-
if (!isInDataGrid && !isInSearchInput) {
|
|
1744
|
-
requestAnimationFrame(() => {
|
|
1745
|
-
dataGridElement.focus()
|
|
1746
|
-
})
|
|
1747
|
-
}
|
|
1748
|
-
return
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
|
|
1752
|
-
const isInDataGrid = dataGridElement.contains(target)
|
|
1753
|
-
if (!isInDataGrid) {
|
|
1754
|
-
return
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
if (key === 'Escape') {
|
|
1758
|
-
const currentState = store.getState()
|
|
1759
|
-
const hasSelections =
|
|
1760
|
-
currentState.selectionState.selectedCells.size > 0 || Object.keys(currentState.rowSelection).length > 0
|
|
1761
|
-
|
|
1762
|
-
if (hasSelections) {
|
|
1763
|
-
event.preventDefault()
|
|
1764
|
-
event.stopPropagation()
|
|
1765
|
-
clearSelection()
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
window.addEventListener('keydown', onGlobalKeyDown, true)
|
|
1771
|
-
return () => {
|
|
1772
|
-
window.removeEventListener('keydown', onGlobalKeyDown, true)
|
|
1773
|
-
}
|
|
1774
|
-
}, [enableSearch, onSearchOpenChange, store, clearSelection])
|
|
1775
|
-
|
|
1776
|
-
React.useEffect(() => {
|
|
1777
|
-
const currentState = store.getState()
|
|
1778
|
-
if (autoFocus && data.length > 0 && columns.length > 0 && !currentState.focusedCell) {
|
|
1779
|
-
const _nav = getNavigableColumnIds()
|
|
1780
|
-
if (_nav.length > 0) {
|
|
1781
|
-
const rafId = requestAnimationFrame(() => {
|
|
1782
|
-
if (typeof autoFocus === 'object') {
|
|
1783
|
-
const { rowIndex, columnId } = autoFocus
|
|
1784
|
-
if (columnId) {
|
|
1785
|
-
focusCell(rowIndex ?? 0, columnId)
|
|
1786
|
-
}
|
|
1787
|
-
return
|
|
1788
|
-
}
|
|
1789
|
-
|
|
1790
|
-
const firstColumnId = _nav[0]
|
|
1791
|
-
if (firstColumnId) {
|
|
1792
|
-
focusCell(0, firstColumnId)
|
|
1793
|
-
}
|
|
1794
|
-
})
|
|
1795
|
-
return () => cancelAnimationFrame(rafId)
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
}, [autoFocus, data.length, columns.length, store, getNavigableColumnIds, focusCell])
|
|
1799
|
-
|
|
1800
|
-
React.useEffect(() => {
|
|
1801
|
-
function onOutsideClick(event: MouseEvent) {
|
|
1802
|
-
if (event.button === 2) {
|
|
1803
|
-
return
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
if (dataGridRef.current && !dataGridRef.current.contains(event.target as Node)) {
|
|
1807
|
-
const target = event.target
|
|
1808
|
-
const isInsidePopover =
|
|
1809
|
-
target instanceof HTMLElement &&
|
|
1810
|
-
(target.closest('[data-grid-cell-editor]') || target.closest('[data-grid-popover]'))
|
|
1811
|
-
|
|
1812
|
-
if (!isInsidePopover) {
|
|
1813
|
-
blurCell()
|
|
1814
|
-
const currentState = store.getState()
|
|
1815
|
-
if (currentState.selectionState.selectedCells.size > 0 || Object.keys(currentState.rowSelection).length > 0) {
|
|
1816
|
-
clearSelection()
|
|
1817
|
-
}
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
document.addEventListener('mousedown', onOutsideClick)
|
|
1823
|
-
return () => {
|
|
1824
|
-
document.removeEventListener('mousedown', onOutsideClick)
|
|
1825
|
-
}
|
|
1826
|
-
}, [store, blurCell, clearSelection])
|
|
1827
|
-
|
|
1828
|
-
React.useEffect(() => {
|
|
1829
|
-
function onCleanup() {
|
|
1830
|
-
document.removeEventListener('selectstart', preventSelection)
|
|
1831
|
-
document.removeEventListener('contextmenu', preventContextMenu)
|
|
1832
|
-
document.body.style.userSelect = ''
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
function preventSelection(event: Event) {
|
|
1836
|
-
event.preventDefault()
|
|
1837
|
-
}
|
|
1838
|
-
function preventContextMenu(event: Event) {
|
|
1839
|
-
event.preventDefault()
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
const onUnsubscribe = store.subscribe(() => {
|
|
1843
|
-
const currentState = store.getState()
|
|
1844
|
-
if (currentState.selectionState.isSelecting) {
|
|
1845
|
-
document.addEventListener('selectstart', preventSelection)
|
|
1846
|
-
document.addEventListener('contextmenu', preventContextMenu)
|
|
1847
|
-
document.body.style.userSelect = 'none'
|
|
1848
|
-
} else {
|
|
1849
|
-
onCleanup()
|
|
1850
|
-
}
|
|
1851
|
-
})
|
|
1852
|
-
|
|
1853
|
-
return () => {
|
|
1854
|
-
onCleanup()
|
|
1855
|
-
onUnsubscribe()
|
|
1856
|
-
}
|
|
1857
|
-
}, [store])
|
|
1858
|
-
|
|
1859
|
-
useIsomorphicLayoutEffect(() => {
|
|
1860
|
-
const rafId = requestAnimationFrame(() => {
|
|
1861
|
-
rowVirtualizer.measure()
|
|
1862
|
-
})
|
|
1863
|
-
return () => cancelAnimationFrame(rafId)
|
|
1864
|
-
}, [
|
|
1865
|
-
data,
|
|
1866
|
-
table.getState().columnFilters,
|
|
1867
|
-
table.getState().columnOrder,
|
|
1868
|
-
table.getState().columnPinning,
|
|
1869
|
-
table.getState().columnSizing,
|
|
1870
|
-
table.getState().columnVisibility,
|
|
1871
|
-
table.getState().expanded,
|
|
1872
|
-
table.getState().globalFilter,
|
|
1873
|
-
table.getState().grouping,
|
|
1874
|
-
table.getState().rowSelection,
|
|
1875
|
-
table.getState().sorting,
|
|
1876
|
-
rowHeight,
|
|
1877
|
-
])
|
|
1878
|
-
|
|
1879
|
-
return {
|
|
1880
|
-
dataGridRef,
|
|
1881
|
-
headerRef,
|
|
1882
|
-
rowMapRef,
|
|
1883
|
-
footerRef,
|
|
1884
|
-
table,
|
|
1885
|
-
rowVirtualizer,
|
|
1886
|
-
searchState,
|
|
1887
|
-
columnSizeVars,
|
|
1888
|
-
onRowAdd: onRowAddProp ? onRowAdd : undefined,
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
export { useDataGrid, type UseDataGridProps }
|