@opencorestack/opengridx 0.1.6 → 0.1.7
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/CHANGELOG.md +15 -0
- package/README.md +9 -0
- package/dist/index.d.ts +6 -0
- package/dist/opengridx.es.js +6 -6
- package/dist/opengridx.es.js.map +1 -1
- package/dist/opengridx.umd.js +4 -4
- package/dist/opengridx.umd.js.map +1 -1
- package/docs/API_REFERENCE.md +177 -0
- package/docs/README.md +60 -0
- package/docs/components/cell.md +44 -0
- package/docs/components/column-group-header.md +57 -0
- package/docs/components/column-resize.md +63 -0
- package/docs/components/column-visibility.md +61 -0
- package/docs/components/datagrid.md +70 -0
- package/docs/components/filter-panel.md +26 -0
- package/docs/components/header.md +39 -0
- package/docs/components/pagination.md +34 -0
- package/docs/components/row.md +31 -0
- package/docs/components/toolbar.md +40 -0
- package/docs/components/tooltip.md +68 -0
- package/docs/customization/slots-api.md +227 -0
- package/docs/customization/theming.md +293 -0
- package/docs/features/aggregation-pivot.md +79 -0
- package/docs/features/cell-spanning.md +520 -0
- package/docs/features/clipboard.md +76 -0
- package/docs/features/custom-pagination.md +143 -0
- package/docs/features/data-source.md +72 -0
- package/docs/features/editing-reordering.md +99 -0
- package/docs/features/export-guide.md +434 -0
- package/docs/features/filtering.md +83 -0
- package/docs/features/infinite-scroll.md +87 -0
- package/docs/features/list-view.md +65 -0
- package/docs/features/loading-states.md +68 -0
- package/docs/features/master-detail.md +46 -0
- package/docs/features/pinning.md +67 -0
- package/docs/features/selection.md +70 -0
- package/docs/features/sorting-pagination.md +66 -0
- package/docs/features/state-persistence.md +67 -0
- package/docs/features/tree-data-grouping.md +67 -0
- package/docs/features/virtualization.md +188 -0
- package/docs/roadmap.md +71 -0
- package/lib/components/Cell/Cell.css +119 -0
- package/lib/components/Cell/Cell.tsx +264 -0
- package/lib/components/Cell/GridEditInputCell.css +62 -0
- package/lib/components/Cell/GridEditInputCell.tsx +152 -0
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.css +67 -0
- package/lib/components/ColumnGroupHeader/ColumnGroupHeader.tsx +220 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.css +31 -0
- package/lib/components/ColumnResizeHandle/ColumnResizeHandle.tsx +85 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.css +137 -0
- package/lib/components/ColumnVisibilityPanel/ColumnVisibilityPanel.tsx +190 -0
- package/lib/components/DataGrid/DataGrid.css +224 -0
- package/lib/components/DataGrid/DataGrid.tsx +2253 -0
- package/lib/components/DetailPanel/DetailPanel.css +36 -0
- package/lib/components/DetailPanel/DetailPanel.tsx +34 -0
- package/lib/components/FilterPanel/FilterPanel.css +196 -0
- package/lib/components/FilterPanel/FilterPanel.tsx +183 -0
- package/lib/components/FilterPanel/index.ts +1 -0
- package/lib/components/Header/ColumnMenu.css +68 -0
- package/lib/components/Header/ColumnMenu.tsx +249 -0
- package/lib/components/Header/Header.css +224 -0
- package/lib/components/Header/Header.tsx +516 -0
- package/lib/components/ListView/ListView.css +112 -0
- package/lib/components/ListView/ListViewRow.tsx +63 -0
- package/lib/components/Pagination/Pagination.css +91 -0
- package/lib/components/Pagination/Pagination.tsx +151 -0
- package/lib/components/QuickFilter/QuickFilter.css +23 -0
- package/lib/components/QuickFilter/QuickFilter.tsx +79 -0
- package/lib/components/Row/Row.css +126 -0
- package/lib/components/Row/Row.tsx +377 -0
- package/lib/components/Skeleton/Skeleton.css +55 -0
- package/lib/components/Skeleton/Skeleton.tsx +22 -0
- package/lib/components/SkeletonRow/SkeletonRow.css +32 -0
- package/lib/components/SkeletonRow/SkeletonRow.tsx +89 -0
- package/lib/components/SkeletonRow/index.ts +1 -0
- package/lib/components/Toolbar/GlobalSearch.css +113 -0
- package/lib/components/Toolbar/GlobalSearch.tsx +120 -0
- package/lib/components/Toolbar/GridToolbar.css +204 -0
- package/lib/components/Toolbar/GridToolbar.tsx +647 -0
- package/lib/components/Toolbar/PivotPanel.css +212 -0
- package/lib/components/Toolbar/PivotPanel.tsx +239 -0
- package/lib/components/Tooltip/Tooltip.css +59 -0
- package/lib/components/Tooltip/Tooltip.tsx +106 -0
- package/lib/components/ui/Button.css +89 -0
- package/lib/components/ui/Button.tsx +39 -0
- package/lib/components/ui/Checkbox.css +71 -0
- package/lib/components/ui/Checkbox.tsx +66 -0
- package/lib/components/ui/DragHandleIcon.css +19 -0
- package/lib/components/ui/DragHandleIcon.tsx +18 -0
- package/lib/components/ui/ExpandIcon.css +38 -0
- package/lib/components/ui/ExpandIcon.tsx +75 -0
- package/lib/components/ui/Input.css +59 -0
- package/lib/components/ui/Input.tsx +45 -0
- package/lib/components/ui/index.ts +15 -0
- package/lib/hooks/core/useDataGrid.ts +392 -0
- package/lib/hooks/core/useGridApiRef.ts +10 -0
- package/lib/hooks/features/useAggregation.ts +143 -0
- package/lib/hooks/features/useGridClipboard.ts +133 -0
- package/lib/hooks/features/useGridDataSource.ts +190 -0
- package/lib/hooks/features/useGridEditing.ts +100 -0
- package/lib/hooks/features/useGridSpanning.ts +349 -0
- package/lib/hooks/features/usePivot.ts +149 -0
- package/lib/hooks/useColumnReorder.ts +114 -0
- package/lib/hooks/useRowGrouping.ts +302 -0
- package/lib/hooks/useRowReorder.ts +83 -0
- package/lib/hooks/useTreeData.ts +323 -0
- package/lib/index.ts +106 -0
- package/lib/state/index.ts +17 -0
- package/lib/state/types.ts +48 -0
- package/lib/state/useGridStateStorage.ts +113 -0
- package/lib/styles/base/reset.css +21 -0
- package/lib/styles/base/variables.css +161 -0
- package/lib/styles/opengridx.css +27 -0
- package/lib/styles/opengridx.css.d.ts +4 -0
- package/lib/theme/DataGridThemeProvider.tsx +276 -0
- package/lib/theme/defaultStyles.ts +0 -0
- package/lib/theme/index.ts +21 -0
- package/lib/theme/presets.ts +148 -0
- package/lib/theme/types.ts +155 -0
- package/lib/types/index.ts +764 -0
- package/lib/utils/export/exportToExcelAdvanced.ts +516 -0
- package/lib/utils/export/index.ts +550 -0
- package/lib/utils/filtering/index.ts +240 -0
- package/lib/utils/pinning/index.ts +282 -0
- package/lib/utils/sorting/index.ts +55 -0
- package/lib/utils/virtualization/index.ts +106 -0
- package/lib/vite-env.d.ts +1 -0
- package/llms.txt +2 -1
- package/package.json +13 -4
package/docs/roadmap.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# OpenGridX Roadmap & Feature Status
|
|
2
|
+
|
|
3
|
+
This document tracks the current status of features in `OpenGridX` and outlines the roadmap for future development.
|
|
4
|
+
|
|
5
|
+
## ✅ Implemented Features
|
|
6
|
+
|
|
7
|
+
### Core
|
|
8
|
+
* **Virtualization**: High-performance row and column virtualization for handling large datasets.
|
|
9
|
+
* **Sorting**: Client-side and server-side multi-column sorting.
|
|
10
|
+
* **Filtering**: Client-side filtering with support for various operators.
|
|
11
|
+
* **Pagination**: Client-side and server-side pagination with customizable page sizes.
|
|
12
|
+
* **Selection**: Row selection (single/multiple) with checkbox support.
|
|
13
|
+
|
|
14
|
+
### Advanced UI
|
|
15
|
+
* **Advanced Filtering**: Client-side complex filter builder (AND/OR logic, recursive groups).
|
|
16
|
+
* **Column Pinning**: Pin columns to the left or right.
|
|
17
|
+
* **Row Pinning**: Pin rows to the top or bottom.
|
|
18
|
+
* **Column Reordering**: Drag-and-drop column reordering.
|
|
19
|
+
* **Column Resizing**: Interactive column resizing.
|
|
20
|
+
* **Detail Panels**: Expandable rows to show custom detail views.
|
|
21
|
+
* **Row Reordering**: Drag-and-drop row reordering.
|
|
22
|
+
|
|
23
|
+
### Data Management
|
|
24
|
+
* **Editing**: Inline cell editing with value parsing/formatting.
|
|
25
|
+
* **Tree Data**: Hierarchical data display (Client-side).
|
|
26
|
+
* **Row Grouping**: Grouping rows by column values (Client-side).
|
|
27
|
+
* **Spanning**: Row and Column spanning (Cell merging).
|
|
28
|
+
* **Export**: CSV, JSON, Print, basic Excel (HTML-table). **Advanced Excel Export** via ExcelJS — real `.xlsx` with styled headers, typed cells (number/date/boolean), column widths, frozen rows, auto-filter, alternating row colors, aggregation totals, and multi-sheet workbooks.
|
|
29
|
+
* **Pivot Mode**: Cross-tabulation of data — group rows into columns dynamically with aggregation (Sum, Avg, Count, Min, Max).
|
|
30
|
+
|
|
31
|
+
### Server-Side Integration
|
|
32
|
+
* **Server-Side Data Source**: Hook-based integration for server-side sorting, filtering, and pagination.
|
|
33
|
+
* **Infinite Scroll**: Automatically load more rows as the user scrolls (`paginationMode='infinite'`).
|
|
34
|
+
* **Server-Side Tree Data**: Lazy loading children nodes, partial hierarchy fetching support.
|
|
35
|
+
* **Server-Side Aggregation**: Sum, Avg, Count, Min, Max — computed server-side over the full dataset, with a sticky footer row and toolbar Σ panel.
|
|
36
|
+
|
|
37
|
+
### CSS & Theming *(completed 2026-02-19)*
|
|
38
|
+
* **CSS Barrel File** (`lib/styles/opengridx.css`): Single entry point — variables → reset → components.
|
|
39
|
+
* **Consistent Naming Convention**: `--ogx-*` CSS variables, `ogx__` BEM class prefix.
|
|
40
|
+
* **Theming API**: `<DataGridThemeProvider theme={myTheme}>` component mapping a `GridTheme` object to `--ogx-*` CSS variables, with built-in light/dark/compact presets.
|
|
41
|
+
* **Production Build Optimisation**: Code-split demo reduces initial bundle by 98.6% via React.lazy + manualChunks.
|
|
42
|
+
|
|
43
|
+
### Accessibility & Clipboard *(completed 2026-03-03)*
|
|
44
|
+
* **ARIA roles**: `role="grid"`, `role="row"`, `role="gridcell"`, `role="columnheader"` on correct elements.
|
|
45
|
+
* **ARIA attributes**: `aria-sort`, `aria-selected`, `aria-label`, `aria-expanded`, `aria-readonly`, `aria-haspopup` throughout.
|
|
46
|
+
* **Keyboard navigation**: Tab / Enter / Escape / Arrow keys / Home / End / PageUp / PageDown.
|
|
47
|
+
* **`aria-rowcount` and `aria-colcount`**: Accurate counts including system columns.
|
|
48
|
+
* **Clipboard**: Full TSV-formatted copy support (Ctrl+C / Cmd+C) for seamless integration with Excel/Google Sheets.
|
|
49
|
+
|
|
50
|
+
### Advanced Excel Export *(completed 2026-03-03)*
|
|
51
|
+
* **`exportToExcelAdvanced`**: Real `.xlsx` via ExcelJS (lazy-loaded — zero bundle cost unless used).
|
|
52
|
+
* **Features**: Styled headers, typed cells, column widths from `colDef.width`, frozen header row, auto-filter dropdowns, alternating row colors, aggregation totals rows, multi-sheet workbooks, standalone summary sheet.
|
|
53
|
+
* **API**: `ExcelAdvancedExportOptions`, `ExcelSheetDefinition`, `ExcelColumnStyle` — all exported and documented.
|
|
54
|
+
* **Demo**: `AdvancedExcelExportDemo` in demo app (`Data Management` category).
|
|
55
|
+
* **Docs**: `docs/features/export-guide.md` fully updated with feature comparison table and all options.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 📅 Upcoming Features
|
|
60
|
+
|
|
61
|
+
### Advanced Data Features
|
|
62
|
+
* **Interactive Pivot Builder**: Drag-and-drop UI for users to dynamically create pivot tables.
|
|
63
|
+
* **Native PDF Reporting**: Automated PDF generation with customizable layouts.
|
|
64
|
+
* **Advanced Charts Integration**: Inline sparklines and trend visualization.
|
|
65
|
+
|
|
66
|
+
### DX & Publishing
|
|
67
|
+
* **npm publish**: Finalize `package.json` metadata, `README.md`, and publish to npm registry.
|
|
68
|
+
|
|
69
|
+
## ✅ Implemented Features (Recent)
|
|
70
|
+
* **Storybook / Docs site**: Interactive component documentation with source code viewer and categorized navigation (implemented via `demo` app).
|
|
71
|
+
* **Quickstart & Installation Guides**: Comprehensive onboarding documentation for developers.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
.ogx__cell {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
align-items: center;
|
|
5
|
+
padding: var(--ogx-grid-cell-padding-y) var(--ogx-grid-cell-padding-x);
|
|
6
|
+
border-right: 1px solid var(--ogx-grid-border-color);
|
|
7
|
+
font-size: var(--ogx-grid-cell-font-size);
|
|
8
|
+
color: var(--ogx-grid-row-text);
|
|
9
|
+
line-height: var(--ogx-grid-line-height);
|
|
10
|
+
white-space: nowrap;
|
|
11
|
+
overflow: hidden;
|
|
12
|
+
text-overflow: ellipsis;
|
|
13
|
+
box-sizing: border-box;
|
|
14
|
+
background-color: inherit;
|
|
15
|
+
}
|
|
16
|
+
.ogx__cell:focus {
|
|
17
|
+
outline: none;
|
|
18
|
+
}
|
|
19
|
+
.ogx__cell--focused {
|
|
20
|
+
/* position tracked internally; ring only shows in keyboard mode (ogx--kb on container) */
|
|
21
|
+
z-index: 10;
|
|
22
|
+
}
|
|
23
|
+
/* Show ring when keyboard navigation is active */
|
|
24
|
+
.ogx--kb .ogx__cell--focused {
|
|
25
|
+
outline: 2px solid var(--ogx-color-primary, #2563eb);
|
|
26
|
+
outline-offset: -2px;
|
|
27
|
+
}
|
|
28
|
+
.ogx__cell--editable {
|
|
29
|
+
cursor: text;
|
|
30
|
+
}
|
|
31
|
+
.ogx__cell--editing {
|
|
32
|
+
padding: 0;
|
|
33
|
+
overflow: visible;
|
|
34
|
+
z-index: 20;
|
|
35
|
+
outline: 2px solid var(--ogx-color-primary);
|
|
36
|
+
outline-offset: -2px;
|
|
37
|
+
background-color: var(--ogx-grid-background);
|
|
38
|
+
}
|
|
39
|
+
.ogx__cell--align-left {
|
|
40
|
+
justify-content: flex-start;
|
|
41
|
+
text-align: left;
|
|
42
|
+
}
|
|
43
|
+
.ogx__cell--align-center {
|
|
44
|
+
justify-content: center;
|
|
45
|
+
text-align: center;
|
|
46
|
+
}
|
|
47
|
+
.ogx__cell--align-right {
|
|
48
|
+
justify-content: flex-end;
|
|
49
|
+
text-align: right;
|
|
50
|
+
}
|
|
51
|
+
.ogx__cell--pinned-left,
|
|
52
|
+
.ogx__cell--pinned-right {
|
|
53
|
+
position: sticky;
|
|
54
|
+
z-index: 2;
|
|
55
|
+
background: inherit;
|
|
56
|
+
background-color: var(--ogx-grid-background);
|
|
57
|
+
}
|
|
58
|
+
.ogx__cell--pinned-right {
|
|
59
|
+
border-left: 1px solid var(--ogx-grid-border-color);
|
|
60
|
+
}
|
|
61
|
+
.ogx:not(.ogx--row-spanning) .ogx__row:hover .ogx__cell--pinned-left,
|
|
62
|
+
.ogx:not(.ogx--row-spanning) .ogx__row:hover .ogx__cell--pinned-right {
|
|
63
|
+
background-color: var(--ogx-grid-row-hover-background);
|
|
64
|
+
}
|
|
65
|
+
.ogx__row--selected .ogx__cell--pinned-left,
|
|
66
|
+
.ogx__row--selected .ogx__cell--pinned-right {
|
|
67
|
+
background-color: var(--ogx-grid-row-selected-background);
|
|
68
|
+
}
|
|
69
|
+
.ogx:not(.ogx--row-spanning) .ogx__row--selected:hover .ogx__cell--pinned-left,
|
|
70
|
+
.ogx:not(.ogx--row-spanning) .ogx__row--selected:hover .ogx__cell--pinned-right {
|
|
71
|
+
background-color: var(--ogx-grid-row-selected-hover-background);
|
|
72
|
+
}
|
|
73
|
+
.ogx__cell--pinned-left-last::after {
|
|
74
|
+
content: '';
|
|
75
|
+
position: absolute;
|
|
76
|
+
top: 0;
|
|
77
|
+
right: -10px;
|
|
78
|
+
bottom: 0;
|
|
79
|
+
width: 10px;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
box-shadow: inset 10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
82
|
+
}
|
|
83
|
+
.ogx__cell--pinned-right-first::before {
|
|
84
|
+
content: '';
|
|
85
|
+
position: absolute;
|
|
86
|
+
top: 0;
|
|
87
|
+
left: -10px;
|
|
88
|
+
bottom: 0;
|
|
89
|
+
width: 10px;
|
|
90
|
+
pointer-events: none;
|
|
91
|
+
box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.1);
|
|
92
|
+
}
|
|
93
|
+
.ogx__cell--hidden {
|
|
94
|
+
visibility: hidden;
|
|
95
|
+
border: none;
|
|
96
|
+
background: transparent;
|
|
97
|
+
}
|
|
98
|
+
.ogx__cell-content {
|
|
99
|
+
overflow: hidden;
|
|
100
|
+
text-overflow: ellipsis;
|
|
101
|
+
white-space: nowrap;
|
|
102
|
+
width: 100%;
|
|
103
|
+
}
|
|
104
|
+
.ogx__cell--spanned {
|
|
105
|
+
background-color: var(--ogx-grid-background);
|
|
106
|
+
z-index: 10;
|
|
107
|
+
}
|
|
108
|
+
.ogx__row--even .ogx__cell--spanned {
|
|
109
|
+
background-color: var(--ogx-grid-row-alternate-background, var(--ogx-grid-background));
|
|
110
|
+
}
|
|
111
|
+
.ogx:not(.ogx--row-spanning) .ogx__row:hover .ogx__cell--spanned {
|
|
112
|
+
background-color: var(--ogx-grid-row-hover-background);
|
|
113
|
+
}
|
|
114
|
+
.ogx__row--selected .ogx__cell--spanned {
|
|
115
|
+
background-color: var(--ogx-grid-row-selected-background);
|
|
116
|
+
}
|
|
117
|
+
.ogx:not(.ogx--row-spanning) .ogx__row--selected:hover .ogx__cell--spanned {
|
|
118
|
+
background-color: var(--ogx-grid-row-selected-hover-background);
|
|
119
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { GridColDef, GridRowModel, GridPinnedPosition, GridCellParams } from '../../types';
|
|
4
|
+
import type { CellColSpanInfo } from '../../hooks/features/useGridSpanning';
|
|
5
|
+
|
|
6
|
+
import { GridEditInputCell } from './GridEditInputCell';
|
|
7
|
+
|
|
8
|
+
export interface CellProps<R extends GridRowModel = GridRowModel> {
|
|
9
|
+
onCellClick?: (params: GridCellParams<R>) => void;
|
|
10
|
+
onCellEditStart?: (field: string, value: any) => void;
|
|
11
|
+
onCellValueChange?: (field: string, newValue: any) => void;
|
|
12
|
+
value: any;
|
|
13
|
+
row: R;
|
|
14
|
+
colDef: GridColDef<R>;
|
|
15
|
+
rowIndex: number;
|
|
16
|
+
colIndex: number;
|
|
17
|
+
isSelected?: boolean;
|
|
18
|
+
onClick?: (event: React.MouseEvent) => void;
|
|
19
|
+
width?: number;
|
|
20
|
+
pinnedPosition?: GridPinnedPosition | null;
|
|
21
|
+
pinnedOffset?: number;
|
|
22
|
+
|
|
23
|
+
isFocused?: boolean;
|
|
24
|
+
isFocusVisible?: boolean;
|
|
25
|
+
|
|
26
|
+
isEditable?: boolean;
|
|
27
|
+
isEditing?: boolean;
|
|
28
|
+
onEditStart?: () => void;
|
|
29
|
+
onEditStop?: (cancel?: boolean) => void;
|
|
30
|
+
onValueChange?: (newValue: any) => void;
|
|
31
|
+
|
|
32
|
+
colSpanInfo?: CellColSpanInfo;
|
|
33
|
+
rowSpan?: number;
|
|
34
|
+
isHiddenByRowSpan?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function CellImpl<R extends GridRowModel = GridRowModel>(props: CellProps<R>) {
|
|
38
|
+
const {
|
|
39
|
+
onCellClick,
|
|
40
|
+
onCellEditStart,
|
|
41
|
+
onCellValueChange,
|
|
42
|
+
value,
|
|
43
|
+
row,
|
|
44
|
+
colDef,
|
|
45
|
+
rowIndex,
|
|
46
|
+
colIndex,
|
|
47
|
+
isSelected,
|
|
48
|
+
onClick,
|
|
49
|
+
width,
|
|
50
|
+
pinnedPosition,
|
|
51
|
+
pinnedOffset,
|
|
52
|
+
isFocused,
|
|
53
|
+
isFocusVisible,
|
|
54
|
+
isEditable,
|
|
55
|
+
isEditing,
|
|
56
|
+
onEditStart,
|
|
57
|
+
onEditStop,
|
|
58
|
+
onValueChange,
|
|
59
|
+
|
|
60
|
+
colSpanInfo,
|
|
61
|
+
rowSpan: rowSpanProp,
|
|
62
|
+
isHiddenByRowSpan
|
|
63
|
+
} = props;
|
|
64
|
+
|
|
65
|
+
if (colSpanInfo?.spannedByColSpan) {
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (isHiddenByRowSpan) {
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div
|
|
74
|
+
className="ogx__cell ogx__cell--hidden"
|
|
75
|
+
style={{ width: width ?? colDef.width ?? 100 }}
|
|
76
|
+
role="presentation"
|
|
77
|
+
data-field={colDef.field}
|
|
78
|
+
data-colindex={colIndex}
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const handleClick = (e: React.MouseEvent) => {
|
|
84
|
+
if (onCellClick) {
|
|
85
|
+
e.stopPropagation();
|
|
86
|
+
onCellClick({
|
|
87
|
+
row,
|
|
88
|
+
field: colDef.field,
|
|
89
|
+
value,
|
|
90
|
+
colDef,
|
|
91
|
+
rowIndex,
|
|
92
|
+
colIndex
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
onClick?.(e);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleValueChange = React.useCallback((newValue: any) => {
|
|
99
|
+
if (onCellValueChange) {
|
|
100
|
+
onCellValueChange(colDef.field, newValue);
|
|
101
|
+
} else {
|
|
102
|
+
onValueChange?.(newValue);
|
|
103
|
+
}
|
|
104
|
+
}, [onCellValueChange, onValueChange, colDef.field]);
|
|
105
|
+
|
|
106
|
+
const handleDoubleClick = (e: React.MouseEvent) => {
|
|
107
|
+
if (isEditable && (onEditStart || onCellEditStart)) {
|
|
108
|
+
e.stopPropagation();
|
|
109
|
+
if (onCellEditStart) {
|
|
110
|
+
onCellEditStart(colDef.field, value);
|
|
111
|
+
} else {
|
|
112
|
+
onEditStart?.();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const formattedValue = React.useMemo(() => {
|
|
118
|
+
if (colDef.valueFormatter) {
|
|
119
|
+
return colDef.valueFormatter({
|
|
120
|
+
value,
|
|
121
|
+
row,
|
|
122
|
+
field: colDef.field
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (value === null || value === undefined) {
|
|
127
|
+
return '';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return String(value);
|
|
131
|
+
}, [value, row, colDef]);
|
|
132
|
+
|
|
133
|
+
// Render custom cell content OR Edit Content
|
|
134
|
+
const cellContent = React.useMemo(() => {
|
|
135
|
+
if (isEditing && (onCellValueChange || onValueChange) && onEditStop) {
|
|
136
|
+
if (colDef.renderEditCell) {
|
|
137
|
+
return colDef.renderEditCell({
|
|
138
|
+
value,
|
|
139
|
+
row,
|
|
140
|
+
field: colDef.field,
|
|
141
|
+
colDef: colDef as any,
|
|
142
|
+
rowIndex,
|
|
143
|
+
colIndex
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return (
|
|
147
|
+
<GridEditInputCell
|
|
148
|
+
value={value}
|
|
149
|
+
row={row}
|
|
150
|
+
field={colDef.field}
|
|
151
|
+
colDef={colDef as any}
|
|
152
|
+
rowIndex={rowIndex}
|
|
153
|
+
colIndex={colIndex}
|
|
154
|
+
onValueChange={handleValueChange}
|
|
155
|
+
onCommit={() => onEditStop()}
|
|
156
|
+
onCancel={() => onEditStop(true)}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (colDef.renderCell) {
|
|
162
|
+
return colDef.renderCell({
|
|
163
|
+
value,
|
|
164
|
+
row,
|
|
165
|
+
field: colDef.field,
|
|
166
|
+
colDef: colDef as any,
|
|
167
|
+
rowIndex,
|
|
168
|
+
colIndex
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return formattedValue;
|
|
173
|
+
}, [value, row, colDef, rowIndex, colIndex, formattedValue, isEditing, handleValueChange, onEditStop, onCellValueChange, onValueChange]);
|
|
174
|
+
|
|
175
|
+
const isPinned = pinnedPosition !== null && pinnedPosition !== undefined;
|
|
176
|
+
|
|
177
|
+
const resolvedCellClassName = React.useMemo(() => {
|
|
178
|
+
if (!colDef.cellClassName) return '';
|
|
179
|
+
if (typeof colDef.cellClassName === 'function') {
|
|
180
|
+
return colDef.cellClassName({ value, row, field: colDef.field, colDef: colDef as any, rowIndex, colIndex }) || '';
|
|
181
|
+
}
|
|
182
|
+
return colDef.cellClassName;
|
|
183
|
+
}, [colDef, value, row, rowIndex, colIndex]);
|
|
184
|
+
|
|
185
|
+
const classNames = [
|
|
186
|
+
'ogx__cell',
|
|
187
|
+
`ogx__cell--align-${colDef.align || 'left'}`,
|
|
188
|
+
isSelected && 'ogx__cell--selected',
|
|
189
|
+
isFocused && 'ogx__cell--focused',
|
|
190
|
+
(isFocused && isFocusVisible) && 'ogx__cell--focus-visible',
|
|
191
|
+
colDef.type && `ogx__cell--type-${colDef.type}`,
|
|
192
|
+
isPinned && 'ogx__cell--pinned',
|
|
193
|
+
isPinned && `ogx__cell--pinned-${pinnedPosition}`,
|
|
194
|
+
isEditing && 'ogx__cell--editing',
|
|
195
|
+
(rowSpanProp && rowSpanProp > 1) && 'ogx__cell--spanned',
|
|
196
|
+
resolvedCellClassName
|
|
197
|
+
].filter(Boolean).join(' ');
|
|
198
|
+
|
|
199
|
+
const colSpan = colSpanInfo?.cellProps?.colSpan || 1;
|
|
200
|
+
const rowSpan = rowSpanProp || 1;
|
|
201
|
+
const cellWidth = colSpanInfo?.cellProps?.width ?? width ?? colDef.width ?? 100;
|
|
202
|
+
|
|
203
|
+
const style: React.CSSProperties = {
|
|
204
|
+
width: 'var(--width)',
|
|
205
|
+
'--width': typeof cellWidth === 'number' ? `${cellWidth}px` : cellWidth,
|
|
206
|
+
flexGrow: colDef.flex ?? 0,
|
|
207
|
+
flexShrink: 0,
|
|
208
|
+
flexBasis: colDef.flex ? 'auto' : 'auto',
|
|
209
|
+
boxSizing: 'border-box',
|
|
210
|
+
minWidth: colDef.minWidth,
|
|
211
|
+
maxWidth: colDef.maxWidth,
|
|
212
|
+
position: isPinned ? 'sticky' : undefined,
|
|
213
|
+
zIndex: colDef.zIndex ?? (isPinned ? 3 : undefined),
|
|
214
|
+
padding: isEditing ? 0 : undefined
|
|
215
|
+
} as React.CSSProperties;
|
|
216
|
+
|
|
217
|
+
if (rowSpan > 1) {
|
|
218
|
+
style.height = `calc(var(--height) * ${rowSpan} - var(--border-width, 1px))`;
|
|
219
|
+
style.zIndex = 10;
|
|
220
|
+
if (isPinned) {
|
|
221
|
+
style.zIndex = 40;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (isPinned && pinnedOffset !== undefined) {
|
|
226
|
+
if (pinnedPosition === 'left') {
|
|
227
|
+
style.left = pinnedOffset;
|
|
228
|
+
} else {
|
|
229
|
+
style.right = pinnedOffset;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const cellRef = React.useRef<HTMLDivElement>(null);
|
|
234
|
+
|
|
235
|
+
React.useLayoutEffect(() => {
|
|
236
|
+
if (isFocused && cellRef.current) {
|
|
237
|
+
cellRef.current.focus({ preventScroll: true });
|
|
238
|
+
}
|
|
239
|
+
}, [isFocused]);
|
|
240
|
+
|
|
241
|
+
return (
|
|
242
|
+
<div
|
|
243
|
+
ref={cellRef}
|
|
244
|
+
className={classNames}
|
|
245
|
+
style={style}
|
|
246
|
+
onClick={handleClick}
|
|
247
|
+
onDoubleClick={handleDoubleClick}
|
|
248
|
+
role="gridcell"
|
|
249
|
+
aria-colindex={colIndex + 1}
|
|
250
|
+
aria-readonly={!isEditable || undefined}
|
|
251
|
+
tabIndex={-1}
|
|
252
|
+
data-field={colDef.field}
|
|
253
|
+
data-colindex={colIndex}
|
|
254
|
+
{...(colSpan > 1 ? { 'aria-colspan': colSpan } : {})}
|
|
255
|
+
{...(rowSpan > 1 ? { 'aria-rowspan': rowSpan } : {})}
|
|
256
|
+
>
|
|
257
|
+
<div className="ogx__cell-content">
|
|
258
|
+
{cellContent}
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const Cell = React.memo(CellImpl) as typeof CellImpl;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* ── Edit cell wrapper ───────────────────────────────────────────────── */
|
|
2
|
+
.ogx__edit-cell {
|
|
3
|
+
width: 100%;
|
|
4
|
+
height: 100%;
|
|
5
|
+
display: flex;
|
|
6
|
+
align-items: center;
|
|
7
|
+
padding: 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* ── Shared input base ───────────────────────────────────────────────── */
|
|
11
|
+
.ogx__edit-input,
|
|
12
|
+
.ogx__edit-select {
|
|
13
|
+
width: 100%;
|
|
14
|
+
height: 100%;
|
|
15
|
+
padding: 0 10px;
|
|
16
|
+
border: none;
|
|
17
|
+
border-radius: 0;
|
|
18
|
+
font-family: var(--ogx-font-family, inherit);
|
|
19
|
+
font-size: var(--ogx-grid-cell-font-size, 14px);
|
|
20
|
+
color: var(--ogx-grid-row-text, #1e293b);
|
|
21
|
+
background: var(--ogx-grid-background, #fff);
|
|
22
|
+
box-sizing: border-box;
|
|
23
|
+
outline: none;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.ogx__edit-input:focus,
|
|
27
|
+
.ogx__edit-select:focus {
|
|
28
|
+
outline: none;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* ── Number input — hide spinners ────────────────────────────────────── */
|
|
32
|
+
.ogx__edit-input--number::-webkit-inner-spin-button,
|
|
33
|
+
.ogx__edit-input--number::-webkit-outer-spin-button {
|
|
34
|
+
-webkit-appearance: none;
|
|
35
|
+
margin: 0;
|
|
36
|
+
}
|
|
37
|
+
.ogx__edit-input--number { -moz-appearance: textfield; appearance: textfield; }
|
|
38
|
+
|
|
39
|
+
/* ── Select ─────────────────────────────────────────────────────────── */
|
|
40
|
+
.ogx__edit-select {
|
|
41
|
+
padding: 0 6px;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
-webkit-appearance: auto;
|
|
44
|
+
appearance: auto;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ── Boolean checkbox ────────────────────────────────────────────────── */
|
|
48
|
+
.ogx__edit-boolean {
|
|
49
|
+
width: 100%;
|
|
50
|
+
height: 100%;
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: center;
|
|
54
|
+
outline: 2px solid var(--ogx-color-primary, #2563eb);
|
|
55
|
+
outline-offset: -2px;
|
|
56
|
+
}
|
|
57
|
+
.ogx__edit-checkbox {
|
|
58
|
+
width: 18px;
|
|
59
|
+
height: 18px;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
accent-color: var(--ogx-color-primary, #2563eb);
|
|
62
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { GridRenderCellParams } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface GridEditInputCellProps extends GridRenderCellParams {
|
|
5
|
+
onValueChange: (value: any) => void;
|
|
6
|
+
onCommit: () => void;
|
|
7
|
+
onCancel: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// ─── Text / String ────────────────────────────────────────────────────────────
|
|
11
|
+
function TextEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
12
|
+
const [local, setLocal] = useState(String(value ?? ''));
|
|
13
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
14
|
+
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<input
|
|
18
|
+
ref={ref}
|
|
19
|
+
type="text"
|
|
20
|
+
className="ogx__edit-input"
|
|
21
|
+
value={local}
|
|
22
|
+
onChange={e => { setLocal(e.target.value); onValueChange(e.target.value); }}
|
|
23
|
+
onBlur={onCommit}
|
|
24
|
+
onKeyDown={e => {
|
|
25
|
+
if (e.key === 'Enter') { e.stopPropagation(); onCommit(); }
|
|
26
|
+
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Number ───────────────────────────────────────────────────────────────────
|
|
33
|
+
function NumberEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
34
|
+
const [local, setLocal] = useState(value ?? '');
|
|
35
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
36
|
+
useEffect(() => { ref.current?.focus(); ref.current?.select(); }, []);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<input
|
|
40
|
+
ref={ref}
|
|
41
|
+
type="number"
|
|
42
|
+
className="ogx__edit-input ogx__edit-input--number"
|
|
43
|
+
value={local}
|
|
44
|
+
onChange={e => {
|
|
45
|
+
const v = e.target.value === '' ? '' : Number(e.target.value);
|
|
46
|
+
setLocal(v);
|
|
47
|
+
onValueChange(v === '' ? null : v);
|
|
48
|
+
}}
|
|
49
|
+
onBlur={onCommit}
|
|
50
|
+
onKeyDown={e => {
|
|
51
|
+
if (e.key === 'Enter') { e.stopPropagation(); onCommit(); }
|
|
52
|
+
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Boolean ──────────────────────────────────────────────────────────────────
|
|
59
|
+
function BooleanEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
60
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
61
|
+
useEffect(() => { ref.current?.focus(); }, []);
|
|
62
|
+
|
|
63
|
+
const handleChange = () => {
|
|
64
|
+
const next = !value;
|
|
65
|
+
onValueChange(next);
|
|
66
|
+
// Commit immediately on toggle
|
|
67
|
+
setTimeout(onCommit, 0);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="ogx__edit-boolean">
|
|
72
|
+
<input
|
|
73
|
+
ref={ref}
|
|
74
|
+
type="checkbox"
|
|
75
|
+
className="ogx__edit-checkbox"
|
|
76
|
+
checked={!!value}
|
|
77
|
+
onChange={handleChange}
|
|
78
|
+
onKeyDown={e => {
|
|
79
|
+
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
80
|
+
}}
|
|
81
|
+
/>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── SingleSelect / Enum ──────────────────────────────────────────────────────
|
|
87
|
+
function SelectEditor({ value, colDef, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
88
|
+
const ref = useRef<HTMLSelectElement>(null);
|
|
89
|
+
useEffect(() => { ref.current?.focus(); }, []);
|
|
90
|
+
|
|
91
|
+
const options = colDef.valueOptions ?? [];
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<select
|
|
95
|
+
ref={ref}
|
|
96
|
+
className="ogx__edit-select"
|
|
97
|
+
value={value ?? ''}
|
|
98
|
+
onChange={e => { onValueChange(e.target.value); }}
|
|
99
|
+
onBlur={onCommit}
|
|
100
|
+
onKeyDown={e => {
|
|
101
|
+
if (e.key === 'Enter') { e.stopPropagation(); onCommit(); }
|
|
102
|
+
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
103
|
+
}}
|
|
104
|
+
>
|
|
105
|
+
{options.map(opt => {
|
|
106
|
+
const v = typeof opt === 'object' ? opt.value : opt;
|
|
107
|
+
const l = typeof opt === 'object' ? opt.label : String(opt);
|
|
108
|
+
return <option key={String(v)} value={String(v)}>{l}</option>;
|
|
109
|
+
})}
|
|
110
|
+
</select>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Date ─────────────────────────────────────────────────────────────────────
|
|
115
|
+
function DateEditor({ value, onValueChange, onCommit, onCancel }: GridEditInputCellProps) {
|
|
116
|
+
const [local, setLocal] = useState(value ?? '');
|
|
117
|
+
const ref = useRef<HTMLInputElement>(null);
|
|
118
|
+
useEffect(() => { ref.current?.focus(); }, []);
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<input
|
|
122
|
+
ref={ref}
|
|
123
|
+
type="date"
|
|
124
|
+
className="ogx__edit-input"
|
|
125
|
+
value={local}
|
|
126
|
+
onChange={e => { setLocal(e.target.value); onValueChange(e.target.value); }}
|
|
127
|
+
onBlur={onCommit}
|
|
128
|
+
onKeyDown={e => {
|
|
129
|
+
if (e.key === 'Enter') { e.stopPropagation(); onCommit(); }
|
|
130
|
+
if (e.key === 'Escape') { e.stopPropagation(); onCancel(); }
|
|
131
|
+
}}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Router ───────────────────────────────────────────────────────────────────
|
|
137
|
+
export function GridEditInputCell(props: GridEditInputCellProps) {
|
|
138
|
+
const { colDef } = props;
|
|
139
|
+
|
|
140
|
+
switch (colDef.type) {
|
|
141
|
+
case 'number':
|
|
142
|
+
return <div className="ogx__edit-cell"><NumberEditor {...props} /></div>;
|
|
143
|
+
case 'boolean':
|
|
144
|
+
return <div className="ogx__edit-cell"><BooleanEditor {...props} /></div>;
|
|
145
|
+
case 'singleSelect':
|
|
146
|
+
return <div className="ogx__edit-cell"><SelectEditor {...props} /></div>;
|
|
147
|
+
case 'date':
|
|
148
|
+
return <div className="ogx__edit-cell"><DateEditor {...props} /></div>;
|
|
149
|
+
default:
|
|
150
|
+
return <div className="ogx__edit-cell"><TextEditor {...props} /></div>;
|
|
151
|
+
}
|
|
152
|
+
}
|