gbs-add-block 1.2.6 → 1.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/index.cjs +10 -9
- package/package.json +1 -1
- package/source/components/button/index.tsx +2 -2
- package/source/components/card/index.tsx +1 -1
- package/source/components/checkbox/index.tsx +1 -1
- package/source/components/datagrid/context/GridContext.tsx +38 -17
- package/source/components/datagrid/createTemplate.tsx +2 -2
- package/source/components/datagrid/hooks/usePagination.ts +0 -8
- package/source/components/datagrid/hooks/useSearch.ts +3 -3
- package/source/components/datagrid/index.tsx +2 -1
- package/source/components/datagrid/layout/GridBody.tsx +7 -4
- package/source/components/datagrid/layout/GridPagination.tsx +3 -2
- package/source/components/datagrid/layout/GridToolbar.tsx +2 -1
- package/source/components/datagrid/type.ts +18 -2
- package/source/components/datagridbeta/README.md +232 -0
- package/source/components/datagridbeta/__tests__/core.test.ts +341 -0
- package/source/components/datagridbeta/__tests__/export.test.ts +70 -0
- package/source/components/datagridbeta/core/columnHelper.ts +21 -0
- package/source/components/datagridbeta/core/columns.ts +288 -0
- package/source/components/datagridbeta/core/filtering.ts +237 -0
- package/source/components/datagridbeta/core/grid.ts +616 -0
- package/source/components/datagridbeta/core/index.ts +14 -0
- package/source/components/datagridbeta/core/rows.ts +64 -0
- package/source/components/datagridbeta/core/sorting.ts +111 -0
- package/source/components/datagridbeta/core/state.ts +82 -0
- package/source/components/datagridbeta/core/store.ts +52 -0
- package/source/components/datagridbeta/core/types.ts +266 -0
- package/source/components/datagridbeta/core/values.ts +98 -0
- package/source/components/datagridbeta/core/virtual.ts +85 -0
- package/source/components/datagridbeta/export/csv.ts +32 -0
- package/source/components/datagridbeta/export/download.ts +12 -0
- package/source/components/datagridbeta/export/pdf.ts +61 -0
- package/source/components/datagridbeta/export/table.ts +73 -0
- package/source/components/datagridbeta/export/xlsx.ts +145 -0
- package/source/components/datagridbeta/export/zip.ts +87 -0
- package/source/components/datagridbeta/index.ts +7 -0
- package/source/components/datagridbeta/react/Cell.tsx +282 -0
- package/source/components/datagridbeta/react/ColumnMenu.tsx +135 -0
- package/source/components/datagridbeta/react/DataGrid.tsx +296 -0
- package/source/components/datagridbeta/react/FilterForm.tsx +142 -0
- package/source/components/datagridbeta/react/HeaderRow.tsx +306 -0
- package/source/components/datagridbeta/react/Pagination.tsx +107 -0
- package/source/components/datagridbeta/react/Popover.tsx +79 -0
- package/source/components/datagridbeta/react/Row.tsx +85 -0
- package/source/components/datagridbeta/react/Toolbar.tsx +266 -0
- package/source/components/datagridbeta/react/Viewport.tsx +167 -0
- package/source/components/datagridbeta/react/context.ts +57 -0
- package/source/components/datagridbeta/react/hooks.ts +80 -0
- package/source/components/datagridbeta/react/icons.tsx +72 -0
- package/source/components/datagridbeta/react/keyboard.ts +110 -0
- package/source/components/datagridbeta/react/locale.ts +128 -0
- package/source/components/datagridbeta/styles.css +717 -0
- package/source/components/datepicker/index.tsx +7 -7
- package/source/components/input/index.tsx +1 -1
- package/source/components/multiselect/PortalDropDown.tsx +1 -1
- package/source/components/multiselect/index.tsx +6 -6
- package/source/components/navbar/AuthSection.tsx +5 -5
- package/source/components/navbar/NavItemComponent.tsx +2 -2
- package/source/components/select/PortalDropDown.tsx +1 -1
- package/source/components/select/index.tsx +5 -5
- package/source/components/spinner/index.tsx +2 -2
- package/source/components/tabs/index.tsx +6 -6
- package/source/components/textarea/index.tsx +9 -9
- package/source/components/toast/ToastComponent.tsx +26 -25
- package/source/components/toast/ToastIcon.tsx +8 -8
- package/source/components/uploader/ProgressAnimation.tsx +2 -2
- package/source/components/uploader/UploadedFilePreview.tsx +1 -1
- package/source/components/uploader/index.tsx +1 -1
- package/source/components/uploader/uploaderIcon.tsx +1 -1
- package/source/globalStyle.ts +29 -16
- package/source/theme.ts +7 -7
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getLayoutVars,
|
|
3
|
+
moveColumn as moveColumnState,
|
|
4
|
+
pinColumn as pinColumnState,
|
|
5
|
+
} from "./columns";
|
|
6
|
+
import type { PageResult } from "./rows";
|
|
7
|
+
import { toggleSorting } from "./sorting";
|
|
8
|
+
import {
|
|
9
|
+
createInitialState,
|
|
10
|
+
mergeState,
|
|
11
|
+
pickQuery,
|
|
12
|
+
queryChanged,
|
|
13
|
+
resetsPage,
|
|
14
|
+
STATE_KEYS,
|
|
15
|
+
} from "./state";
|
|
16
|
+
import { createStore } from "./store";
|
|
17
|
+
import type {
|
|
18
|
+
ColumnFilter,
|
|
19
|
+
ColumnLayout,
|
|
20
|
+
Density,
|
|
21
|
+
ExportOptions,
|
|
22
|
+
ExportScope,
|
|
23
|
+
GridOptions,
|
|
24
|
+
GridRow,
|
|
25
|
+
GridState,
|
|
26
|
+
PdfExportOptions,
|
|
27
|
+
PinSide,
|
|
28
|
+
ResolvedColumn,
|
|
29
|
+
RowId,
|
|
30
|
+
SortItem,
|
|
31
|
+
} from "./types";
|
|
32
|
+
import { formatCellValue, type Formatters } from "./values";
|
|
33
|
+
import type { Viewport } from "./virtual";
|
|
34
|
+
|
|
35
|
+
export interface CellPosition {
|
|
36
|
+
/** -1 is the header row. */
|
|
37
|
+
rowIndex: number;
|
|
38
|
+
/** Index into `layout.columns` (visual order). */
|
|
39
|
+
colIndex: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UiState {
|
|
43
|
+
activeCell: CellPosition | null;
|
|
44
|
+
editing: { rowId: RowId; columnId: string } | null;
|
|
45
|
+
/** Values shown while an async `onCellEdit` is in flight. */
|
|
46
|
+
pending: Record<RowId, Record<string, unknown>>;
|
|
47
|
+
/** Errors from rejected async edits. */
|
|
48
|
+
errors: Record<RowId, Record<string, string>>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GridSnapshot {
|
|
52
|
+
/** Internal (uncontrolled) state. Controlled props are merged in by the React layer. */
|
|
53
|
+
state: GridState;
|
|
54
|
+
ui: UiState;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Everything derived during render, handed to the engine after each commit. */
|
|
58
|
+
export interface GridModel<T> {
|
|
59
|
+
columns: ResolvedColumn<T>[];
|
|
60
|
+
coreRows: GridRow<T>[];
|
|
61
|
+
/** Filtered and sorted rows (client mode); the current data in server mode. */
|
|
62
|
+
sortedRows: GridRow<T>[];
|
|
63
|
+
page: PageResult<T>;
|
|
64
|
+
layout: ColumnLayout<T>;
|
|
65
|
+
rowHeight: number;
|
|
66
|
+
headerHeight: number;
|
|
67
|
+
formatters: Formatters;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type MoveDirection = "up" | "down" | "left" | "right";
|
|
71
|
+
|
|
72
|
+
/** Imperative API, available through the grid's `ref`. */
|
|
73
|
+
export interface GridApi<T> {
|
|
74
|
+
getState(): GridState;
|
|
75
|
+
setState(updater: (prev: GridState) => GridState): void;
|
|
76
|
+
|
|
77
|
+
toggleSort(columnId: string, multi?: boolean): void;
|
|
78
|
+
setSorting(sorting: SortItem[]): void;
|
|
79
|
+
setFilter(columnId: string, filter: Omit<ColumnFilter, "columnId"> | null): void;
|
|
80
|
+
clearFilters(): void;
|
|
81
|
+
setGlobalFilter(value: string): void;
|
|
82
|
+
setPageIndex(pageIndex: number): void;
|
|
83
|
+
setPageSize(pageSize: number): void;
|
|
84
|
+
setDensity(density: Density): void;
|
|
85
|
+
|
|
86
|
+
isRowSelectable(row: T): boolean;
|
|
87
|
+
toggleRowSelected(rowId: RowId, options?: { value?: boolean; range?: boolean }): void;
|
|
88
|
+
/** Selects (or deselects) every row matching the current filters. */
|
|
89
|
+
toggleAllRowsSelected(value: boolean): void;
|
|
90
|
+
clearSelection(): void;
|
|
91
|
+
getSelectedRowIds(): RowId[];
|
|
92
|
+
getSelectedRows(): T[];
|
|
93
|
+
|
|
94
|
+
setColumnVisibility(columnId: string, visible: boolean): void;
|
|
95
|
+
setColumnWidth(columnId: string, width: number | null): void;
|
|
96
|
+
pinColumn(columnId: string, side: PinSide | false): void;
|
|
97
|
+
moveColumn(columnId: string, targetId: string, placement: "before" | "after"): void;
|
|
98
|
+
resetColumns(): void;
|
|
99
|
+
|
|
100
|
+
scrollToRow(rowIndex: number): void;
|
|
101
|
+
focusCell(rowIndex: number, columnId: string): void;
|
|
102
|
+
startEditing(rowId: RowId, columnId: string): void;
|
|
103
|
+
cancelEditing(): void;
|
|
104
|
+
|
|
105
|
+
getRows(scope?: ExportScope): T[];
|
|
106
|
+
exportCsv(options?: ExportOptions<T>): Promise<void>;
|
|
107
|
+
exportExcel(options?: ExportOptions<T>): Promise<void>;
|
|
108
|
+
exportPdf(options?: PdfExportOptions<T>): Promise<void>;
|
|
109
|
+
/** Copies selected rows as TSV, or the active cell when nothing is selected. */
|
|
110
|
+
copyToClipboard(): Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const EMPTY_UI: UiState = { activeCell: null, editing: null, pending: {}, errors: {} };
|
|
114
|
+
|
|
115
|
+
function setNested<V>(
|
|
116
|
+
record: Record<RowId, Record<string, V>>,
|
|
117
|
+
rowId: RowId,
|
|
118
|
+
columnId: string,
|
|
119
|
+
value: V | undefined,
|
|
120
|
+
): Record<RowId, Record<string, V>> {
|
|
121
|
+
if (value === undefined && record[rowId]?.[columnId] === undefined) return record;
|
|
122
|
+
const inner = { ...record[rowId] };
|
|
123
|
+
if (value === undefined) delete inner[columnId];
|
|
124
|
+
else inner[columnId] = value;
|
|
125
|
+
const next = { ...record };
|
|
126
|
+
if (Object.keys(inner).length > 0) next[rowId] = inner;
|
|
127
|
+
else delete next[rowId];
|
|
128
|
+
return next;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
132
|
+
Math.min(max, Math.max(min, value));
|
|
133
|
+
|
|
134
|
+
export function createGridEngine<T>(initialOptions: GridOptions<T>) {
|
|
135
|
+
let options = initialOptions;
|
|
136
|
+
let model: GridModel<T> | null = null;
|
|
137
|
+
let viewportEl: HTMLElement | null = null;
|
|
138
|
+
let focusRequested = false;
|
|
139
|
+
let anchorRowId: RowId | null = null;
|
|
140
|
+
let draggingColumnId: string | null = null;
|
|
141
|
+
let rowLookup: { rows: GridRow<T>[]; map: Map<RowId, GridRow<T>> } | null = null;
|
|
142
|
+
|
|
143
|
+
const store = createStore<GridSnapshot>({
|
|
144
|
+
state: createInitialState(options),
|
|
145
|
+
ui: EMPTY_UI,
|
|
146
|
+
});
|
|
147
|
+
const viewport = createStore<Viewport>({ scrollTop: 0, scrollLeft: 0, width: 0, height: 0 });
|
|
148
|
+
|
|
149
|
+
const getState = () => mergeState(store.getSnapshot().state, options.state);
|
|
150
|
+
|
|
151
|
+
const setUi = (updater: (ui: UiState) => UiState) =>
|
|
152
|
+
store.setState((snapshot) => {
|
|
153
|
+
const ui = updater(snapshot.ui);
|
|
154
|
+
return ui === snapshot.ui ? snapshot : { ...snapshot, ui };
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
function setState(updater: (prev: GridState) => GridState) {
|
|
158
|
+
const prev = getState();
|
|
159
|
+
let next = updater(prev);
|
|
160
|
+
if (next === prev) return;
|
|
161
|
+
|
|
162
|
+
if (
|
|
163
|
+
resetsPage(prev, next) &&
|
|
164
|
+
next.pagination === prev.pagination &&
|
|
165
|
+
prev.pagination.pageIndex !== 0
|
|
166
|
+
) {
|
|
167
|
+
next = { ...next, pagination: { ...next.pagination, pageIndex: 0 } };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const controlled = options.state ?? {};
|
|
171
|
+
store.setState((snapshot) => {
|
|
172
|
+
let internal = snapshot.state;
|
|
173
|
+
for (const key of STATE_KEYS) {
|
|
174
|
+
if (next[key] !== prev[key] && controlled[key] === undefined) {
|
|
175
|
+
if (internal === snapshot.state) internal = { ...internal };
|
|
176
|
+
(internal as unknown as Record<string, unknown>)[key] = next[key];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return internal === snapshot.state ? snapshot : { ...snapshot, state: internal };
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (next.pagination.pageIndex !== prev.pagination.pageIndex && viewportEl) {
|
|
183
|
+
viewportEl.scrollTop = 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
options.onStateChange?.(next, prev);
|
|
187
|
+
if (queryChanged(prev, next)) options.onQueryChange?.(pickQuery(next));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const update = <K extends keyof GridState>(
|
|
191
|
+
key: K,
|
|
192
|
+
compute: (value: GridState[K]) => GridState[K],
|
|
193
|
+
) =>
|
|
194
|
+
setState((prev) => {
|
|
195
|
+
const value = compute(prev[key]);
|
|
196
|
+
return value === prev[key] ? prev : { ...prev, [key]: value };
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const getRowById = (rowId: RowId) => {
|
|
200
|
+
const rows = model?.coreRows ?? [];
|
|
201
|
+
if (rowLookup?.rows !== rows) {
|
|
202
|
+
rowLookup = { rows, map: new Map(rows.map((row) => [row.id, row])) };
|
|
203
|
+
}
|
|
204
|
+
return rowLookup.map.get(rowId);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const getColumn = (columnId: string) =>
|
|
208
|
+
model?.columns.find((column) => column.id === columnId);
|
|
209
|
+
|
|
210
|
+
// ---------------------------------------------------------------- selection
|
|
211
|
+
|
|
212
|
+
const isRowSelectable = (row: T) => {
|
|
213
|
+
const enabled = options.enableRowSelection;
|
|
214
|
+
return typeof enabled === "function" ? enabled(row) : enabled === true;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
function toggleRowSelected(
|
|
218
|
+
rowId: RowId,
|
|
219
|
+
{ value, range = false }: { value?: boolean; range?: boolean } = {},
|
|
220
|
+
) {
|
|
221
|
+
const row = getRowById(rowId);
|
|
222
|
+
if (!row || !isRowSelectable(row.original)) return;
|
|
223
|
+
|
|
224
|
+
update("rowSelection", (selection) => {
|
|
225
|
+
const target = value ?? !selection[rowId];
|
|
226
|
+
if (options.selectionMode === "single") {
|
|
227
|
+
return target ? { [rowId]: true } : {};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const next = { ...selection };
|
|
231
|
+
const rows = model?.sortedRows ?? [];
|
|
232
|
+
const from = range && anchorRowId !== null ? rows.findIndex((r) => r.id === anchorRowId) : -1;
|
|
233
|
+
const to = from === -1 ? -1 : rows.findIndex((r) => r.id === rowId);
|
|
234
|
+
|
|
235
|
+
if (from !== -1 && to !== -1) {
|
|
236
|
+
for (let i = Math.min(from, to); i <= Math.max(from, to); i++) {
|
|
237
|
+
if (!isRowSelectable(rows[i].original)) continue;
|
|
238
|
+
if (target) next[rows[i].id] = true;
|
|
239
|
+
else delete next[rows[i].id];
|
|
240
|
+
}
|
|
241
|
+
} else if (target) {
|
|
242
|
+
next[rowId] = true;
|
|
243
|
+
} else {
|
|
244
|
+
delete next[rowId];
|
|
245
|
+
}
|
|
246
|
+
return next;
|
|
247
|
+
});
|
|
248
|
+
anchorRowId = rowId;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function toggleAllRowsSelected(value: boolean) {
|
|
252
|
+
if (options.selectionMode === "single") return;
|
|
253
|
+
const rows = model?.sortedRows ?? [];
|
|
254
|
+
update("rowSelection", (selection) => {
|
|
255
|
+
const next = { ...selection };
|
|
256
|
+
for (const row of rows) {
|
|
257
|
+
if (!isRowSelectable(row.original)) continue;
|
|
258
|
+
if (value) next[row.id] = true;
|
|
259
|
+
else delete next[row.id];
|
|
260
|
+
}
|
|
261
|
+
return next;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const getSelectedRowIds = () =>
|
|
266
|
+
Object.keys(getState().rowSelection).filter((id) => getState().rowSelection[id]);
|
|
267
|
+
|
|
268
|
+
const getSelectedRows = () => {
|
|
269
|
+
const selection = getState().rowSelection;
|
|
270
|
+
return (model?.coreRows ?? []).filter((row) => selection[row.id]).map((row) => row.original);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// --------------------------------------------------------------- navigation
|
|
274
|
+
|
|
275
|
+
function ensureVisible(rowIndex: number, colIndex: number) {
|
|
276
|
+
const el = viewportEl;
|
|
277
|
+
if (!el || !model) return;
|
|
278
|
+
const { layout, rowHeight, headerHeight } = model;
|
|
279
|
+
|
|
280
|
+
if (rowIndex >= 0) {
|
|
281
|
+
const top = rowIndex * rowHeight;
|
|
282
|
+
const bodyHeight = el.clientHeight - headerHeight;
|
|
283
|
+
if (top < el.scrollTop) el.scrollTop = top;
|
|
284
|
+
else if (top + rowHeight > el.scrollTop + bodyHeight) {
|
|
285
|
+
el.scrollTop = top + rowHeight - bodyHeight;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const item = layout.columns[colIndex];
|
|
290
|
+
if (item && !item.pinned) {
|
|
291
|
+
const rtl = getComputedStyle(el).direction === "rtl";
|
|
292
|
+
const scroll = Math.abs(el.scrollLeft);
|
|
293
|
+
const start = item.offset;
|
|
294
|
+
const end = start + item.width;
|
|
295
|
+
const visibleWidth = el.clientWidth - layout.leftWidth - layout.rightWidth;
|
|
296
|
+
let nextScroll = scroll;
|
|
297
|
+
if (start < scroll) nextScroll = start;
|
|
298
|
+
else if (end > scroll + visibleWidth) nextScroll = end - visibleWidth;
|
|
299
|
+
if (nextScroll !== scroll) el.scrollLeft = rtl ? -nextScroll : nextScroll;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Scroll events are async; update the range now so the target renders this frame.
|
|
303
|
+
viewport.setState((v) =>
|
|
304
|
+
v.scrollTop === el.scrollTop && v.scrollLeft === el.scrollLeft
|
|
305
|
+
? v
|
|
306
|
+
: { ...v, scrollTop: el.scrollTop, scrollLeft: el.scrollLeft },
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function setActiveCell(
|
|
311
|
+
rowIndex: number,
|
|
312
|
+
colIndex: number,
|
|
313
|
+
{ focus = true, scroll = true }: { focus?: boolean; scroll?: boolean } = {},
|
|
314
|
+
) {
|
|
315
|
+
if (!model || model.layout.columns.length === 0) return;
|
|
316
|
+
const position = {
|
|
317
|
+
rowIndex: clamp(rowIndex, -1, model.page.rows.length - 1),
|
|
318
|
+
colIndex: clamp(colIndex, 0, model.layout.columns.length - 1),
|
|
319
|
+
};
|
|
320
|
+
if (focus) focusRequested = true;
|
|
321
|
+
setUi((ui) =>
|
|
322
|
+
ui.activeCell?.rowIndex === position.rowIndex &&
|
|
323
|
+
ui.activeCell.colIndex === position.colIndex
|
|
324
|
+
? ui
|
|
325
|
+
: { ...ui, activeCell: position },
|
|
326
|
+
);
|
|
327
|
+
if (scroll) ensureVisible(position.rowIndex, position.colIndex);
|
|
328
|
+
// Already active: nothing re-renders, so focus the element directly.
|
|
329
|
+
if (focus && viewportEl) {
|
|
330
|
+
const selector = `[data-row-index="${position.rowIndex}"] [data-col-index="${position.colIndex}"]`;
|
|
331
|
+
const cell = viewportEl.querySelector<HTMLElement>(selector);
|
|
332
|
+
if (cell && document.activeElement !== cell) {
|
|
333
|
+
cell.focus({ preventScroll: true });
|
|
334
|
+
focusRequested = false;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function moveActiveCell(direction: MoveDirection, by = 1) {
|
|
340
|
+
const current = store.getSnapshot().ui.activeCell ?? { rowIndex: -1, colIndex: 0 };
|
|
341
|
+
const dRow = direction === "up" ? -by : direction === "down" ? by : 0;
|
|
342
|
+
const dCol = direction === "left" ? -by : direction === "right" ? by : 0;
|
|
343
|
+
setActiveCell(current.rowIndex + dRow, current.colIndex + dCol);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ------------------------------------------------------------------ editing
|
|
347
|
+
|
|
348
|
+
const isCellEditable = (row: T, column: ResolvedColumn<T>) => {
|
|
349
|
+
const { editable } = column.def;
|
|
350
|
+
return typeof editable === "function" ? editable(row) : editable === true;
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
function startEditing(rowId: RowId, columnId: string) {
|
|
354
|
+
const row = getRowById(rowId);
|
|
355
|
+
const column = getColumn(columnId);
|
|
356
|
+
if (!row || !column || !isCellEditable(row.original, column)) return;
|
|
357
|
+
setUi((ui) => ({ ...ui, editing: { rowId, columnId } }));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function cancelEditing() {
|
|
361
|
+
if (!store.getSnapshot().ui.editing) return;
|
|
362
|
+
focusRequested = true;
|
|
363
|
+
setUi((ui) => ({ ...ui, editing: null }));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Returns a validation error, or null when the edit was accepted. */
|
|
367
|
+
function commitEdit(
|
|
368
|
+
rowId: RowId,
|
|
369
|
+
columnId: string,
|
|
370
|
+
value: unknown,
|
|
371
|
+
move?: MoveDirection,
|
|
372
|
+
): string | null {
|
|
373
|
+
const editing = store.getSnapshot().ui.editing;
|
|
374
|
+
if (editing?.rowId !== rowId || editing.columnId !== columnId) return null;
|
|
375
|
+
|
|
376
|
+
const row = getRowById(rowId);
|
|
377
|
+
const column = getColumn(columnId);
|
|
378
|
+
if (!row || !column) return null;
|
|
379
|
+
|
|
380
|
+
const error = column.def.validate?.(value, row.original);
|
|
381
|
+
if (error) return error;
|
|
382
|
+
|
|
383
|
+
focusRequested = true;
|
|
384
|
+
setUi((ui) => ({ ...ui, editing: null, errors: setNested(ui.errors, rowId, columnId, undefined) }));
|
|
385
|
+
|
|
386
|
+
const previousValue = column.getValue(row.original);
|
|
387
|
+
if (!Object.is(previousValue, value) && options.onCellEdit) {
|
|
388
|
+
const result = options.onCellEdit({ row: row.original, rowId, columnId, value, previousValue });
|
|
389
|
+
if (result instanceof Promise) {
|
|
390
|
+
setUi((ui) => ({ ...ui, pending: setNested(ui.pending, rowId, columnId, value) }));
|
|
391
|
+
result.then(
|
|
392
|
+
() => setUi((ui) => ({ ...ui, pending: setNested(ui.pending, rowId, columnId, undefined) })),
|
|
393
|
+
(reason: unknown) =>
|
|
394
|
+
setUi((ui) => ({
|
|
395
|
+
...ui,
|
|
396
|
+
pending: setNested(ui.pending, rowId, columnId, undefined),
|
|
397
|
+
errors: setNested(
|
|
398
|
+
ui.errors,
|
|
399
|
+
rowId,
|
|
400
|
+
columnId,
|
|
401
|
+
reason instanceof Error ? reason.message : String(reason),
|
|
402
|
+
),
|
|
403
|
+
})),
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
if (move) moveActiveCell(move);
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ------------------------------------------------------------------ columns
|
|
413
|
+
|
|
414
|
+
function resetColumns() {
|
|
415
|
+
const initial = createInitialState({ ...options, initialState: undefined });
|
|
416
|
+
setState((prev) => ({
|
|
417
|
+
...prev,
|
|
418
|
+
columnOrder: initial.columnOrder,
|
|
419
|
+
columnVisibility: initial.columnVisibility,
|
|
420
|
+
columnSizing: initial.columnSizing,
|
|
421
|
+
columnPinning: initial.columnPinning,
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Live resize: rewrites CSS variables without rendering. */
|
|
426
|
+
function previewColumnWidth(columnId: string, width: number) {
|
|
427
|
+
if (!viewportEl || !model) return;
|
|
428
|
+
const vars = getLayoutVars(model.layout, { [columnId]: width });
|
|
429
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
430
|
+
viewportEl.style.setProperty(name, value);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// ------------------------------------------------------------------- export
|
|
435
|
+
|
|
436
|
+
function getRows(scope: ExportScope = "filtered"): T[] {
|
|
437
|
+
if (!model) return [];
|
|
438
|
+
switch (scope) {
|
|
439
|
+
case "all":
|
|
440
|
+
return model.coreRows.map((row) => row.original);
|
|
441
|
+
case "page":
|
|
442
|
+
return model.page.rows.map((row) => row.original);
|
|
443
|
+
case "selected":
|
|
444
|
+
return getSelectedRows();
|
|
445
|
+
default:
|
|
446
|
+
return model.sortedRows.map((row) => row.original);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function buildTable(exportOptions: ExportOptions<T>) {
|
|
451
|
+
const { buildExportTable } = await import("../export/table");
|
|
452
|
+
const rows = exportOptions.rows ?? getRows(exportOptions.scope);
|
|
453
|
+
const items = (model?.layout.columns ?? []).filter(
|
|
454
|
+
(item) => !item.column.internal && item.column.exportable,
|
|
455
|
+
);
|
|
456
|
+
return buildExportTable(rows, items, model!.formatters);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const fileName = (exportOptions: ExportOptions<T>) =>
|
|
460
|
+
exportOptions.fileName ?? options.exportFileName ?? "export";
|
|
461
|
+
|
|
462
|
+
const api: GridApi<T> = {
|
|
463
|
+
getState,
|
|
464
|
+
setState,
|
|
465
|
+
|
|
466
|
+
toggleSort: (columnId, multi = false) =>
|
|
467
|
+
update("sorting", (sorting) =>
|
|
468
|
+
toggleSorting(sorting, columnId, multi && options.enableMultiSort !== false),
|
|
469
|
+
),
|
|
470
|
+
setSorting: (sorting) => update("sorting", () => sorting),
|
|
471
|
+
setFilter: (columnId, filter) =>
|
|
472
|
+
update("filters", (filters) => {
|
|
473
|
+
const rest = filters.filter((f) => f.columnId !== columnId);
|
|
474
|
+
if (!filter) return rest.length === filters.length ? filters : rest;
|
|
475
|
+
return [...rest, { ...filter, columnId }];
|
|
476
|
+
}),
|
|
477
|
+
clearFilters: () =>
|
|
478
|
+
setState((prev) =>
|
|
479
|
+
prev.filters.length === 0 && prev.globalFilter === ""
|
|
480
|
+
? prev
|
|
481
|
+
: { ...prev, filters: [], globalFilter: "" },
|
|
482
|
+
),
|
|
483
|
+
setGlobalFilter: (value) => update("globalFilter", () => value),
|
|
484
|
+
setPageIndex: (pageIndex) =>
|
|
485
|
+
update("pagination", (p) =>
|
|
486
|
+
p.pageIndex === pageIndex ? p : { ...p, pageIndex: Math.max(0, pageIndex) },
|
|
487
|
+
),
|
|
488
|
+
setPageSize: (pageSize) =>
|
|
489
|
+
update("pagination", (p) =>
|
|
490
|
+
p.pageSize === pageSize
|
|
491
|
+
? p
|
|
492
|
+
: { pageSize, pageIndex: Math.floor((p.pageIndex * p.pageSize) / pageSize) },
|
|
493
|
+
),
|
|
494
|
+
setDensity: (density) => update("density", () => density),
|
|
495
|
+
|
|
496
|
+
isRowSelectable,
|
|
497
|
+
toggleRowSelected,
|
|
498
|
+
toggleAllRowsSelected,
|
|
499
|
+
clearSelection: () =>
|
|
500
|
+
update("rowSelection", (selection) =>
|
|
501
|
+
Object.keys(selection).length === 0 ? selection : {},
|
|
502
|
+
),
|
|
503
|
+
getSelectedRowIds,
|
|
504
|
+
getSelectedRows,
|
|
505
|
+
|
|
506
|
+
setColumnVisibility: (columnId, visible) =>
|
|
507
|
+
update("columnVisibility", (visibility) =>
|
|
508
|
+
(visibility[columnId] !== false) === visible
|
|
509
|
+
? visibility
|
|
510
|
+
: { ...visibility, [columnId]: visible },
|
|
511
|
+
),
|
|
512
|
+
setColumnWidth: (columnId, width) =>
|
|
513
|
+
update("columnSizing", (sizing) => {
|
|
514
|
+
const next = { ...sizing };
|
|
515
|
+
if (width === null) delete next[columnId];
|
|
516
|
+
else next[columnId] = Math.round(width);
|
|
517
|
+
return next;
|
|
518
|
+
}),
|
|
519
|
+
pinColumn: (columnId, side) =>
|
|
520
|
+
update("columnPinning", (pinning) => pinColumnState(pinning, columnId, side)),
|
|
521
|
+
moveColumn: (columnId, targetId, placement) =>
|
|
522
|
+
setState((prev) => ({
|
|
523
|
+
...prev,
|
|
524
|
+
...moveColumnState(model?.columns ?? [], prev, columnId, targetId, placement),
|
|
525
|
+
})),
|
|
526
|
+
resetColumns,
|
|
527
|
+
|
|
528
|
+
scrollToRow: (rowIndex) => ensureVisible(rowIndex, -1),
|
|
529
|
+
focusCell: (rowIndex, columnId) => {
|
|
530
|
+
const colIndex = model?.layout.columns.findIndex((item) => item.column.id === columnId) ?? -1;
|
|
531
|
+
if (colIndex !== -1) setActiveCell(rowIndex, colIndex);
|
|
532
|
+
},
|
|
533
|
+
startEditing,
|
|
534
|
+
cancelEditing,
|
|
535
|
+
|
|
536
|
+
getRows,
|
|
537
|
+
async exportCsv(exportOptions = {}) {
|
|
538
|
+
const [{ createCsvBlob }, { downloadBlob }, table] = await Promise.all([
|
|
539
|
+
import("../export/csv"),
|
|
540
|
+
import("../export/download"),
|
|
541
|
+
buildTable(exportOptions),
|
|
542
|
+
]);
|
|
543
|
+
downloadBlob(createCsvBlob(table), `${fileName(exportOptions)}.csv`);
|
|
544
|
+
},
|
|
545
|
+
async exportExcel(exportOptions = {}) {
|
|
546
|
+
const [{ createXlsxBlob }, { downloadBlob }, table] = await Promise.all([
|
|
547
|
+
import("../export/xlsx"),
|
|
548
|
+
import("../export/download"),
|
|
549
|
+
buildTable(exportOptions),
|
|
550
|
+
]);
|
|
551
|
+
downloadBlob(createXlsxBlob(table), `${fileName(exportOptions)}.xlsx`);
|
|
552
|
+
},
|
|
553
|
+
async exportPdf(exportOptions = {}) {
|
|
554
|
+
const [{ printTable }, table] = await Promise.all([
|
|
555
|
+
import("../export/pdf"),
|
|
556
|
+
buildTable(exportOptions),
|
|
557
|
+
]);
|
|
558
|
+
printTable(table, {
|
|
559
|
+
title: exportOptions.title ?? fileName(exportOptions),
|
|
560
|
+
orientation: exportOptions.orientation ?? "landscape",
|
|
561
|
+
paperSize: exportOptions.paperSize ?? "A4",
|
|
562
|
+
});
|
|
563
|
+
},
|
|
564
|
+
async copyToClipboard() {
|
|
565
|
+
if (!model) return;
|
|
566
|
+
const selected = getSelectedRows();
|
|
567
|
+
let text = "";
|
|
568
|
+
if (selected.length > 0) {
|
|
569
|
+
const { toTsv } = await import("../export/csv");
|
|
570
|
+
text = toTsv(await buildTable({ rows: selected }));
|
|
571
|
+
} else {
|
|
572
|
+
const active = store.getSnapshot().ui.activeCell;
|
|
573
|
+
const row = active && model.page.rows[active.rowIndex];
|
|
574
|
+
const item = active && model.layout.columns[active.colIndex];
|
|
575
|
+
if (row && item && !item.column.internal) {
|
|
576
|
+
const value = item.column.getValue(row.original);
|
|
577
|
+
text = formatCellValue(item.column, value, row.original, model.formatters);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (text) await navigator.clipboard.writeText(text);
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
return {
|
|
585
|
+
api,
|
|
586
|
+
store,
|
|
587
|
+
viewport,
|
|
588
|
+
getOptions: () => options,
|
|
589
|
+
getModel: () => model,
|
|
590
|
+
/** Called after every commit with the latest props and derived model. */
|
|
591
|
+
sync(nextOptions: GridOptions<T>, nextModel: GridModel<T>) {
|
|
592
|
+
options = nextOptions;
|
|
593
|
+
model = nextModel;
|
|
594
|
+
},
|
|
595
|
+
attachViewport(el: HTMLElement | null) {
|
|
596
|
+
viewportEl = el;
|
|
597
|
+
},
|
|
598
|
+
setActiveCell,
|
|
599
|
+
moveActiveCell,
|
|
600
|
+
consumeFocusRequest() {
|
|
601
|
+
const requested = focusRequested;
|
|
602
|
+
focusRequested = false;
|
|
603
|
+
return requested;
|
|
604
|
+
},
|
|
605
|
+
isRowSelectable,
|
|
606
|
+
isCellEditable,
|
|
607
|
+
commitEdit,
|
|
608
|
+
previewColumnWidth,
|
|
609
|
+
getDraggingColumn: () => draggingColumnId,
|
|
610
|
+
setDraggingColumn(columnId: string | null) {
|
|
611
|
+
draggingColumnId = columnId;
|
|
612
|
+
},
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export type GridEngine<T> = ReturnType<typeof createGridEngine<T>>;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Framework-free entry point: safe to import on the server (e.g. to sort,
|
|
2
|
+
// filter and paginate in a Next.js route handler with the same semantics).
|
|
3
|
+
export { createColumnHelper } from "./columnHelper";
|
|
4
|
+
export { computeLayout, resolveColumns } from "./columns";
|
|
5
|
+
export { compileFilter, filterRows, getFilterOperators, isFilterActive } from "./filtering";
|
|
6
|
+
export { createGridEngine } from "./grid";
|
|
7
|
+
export type { GridApi, GridEngine } from "./grid";
|
|
8
|
+
export { buildRows, createRowIdGetter, paginate } from "./rows";
|
|
9
|
+
export type { PageResult } from "./rows";
|
|
10
|
+
export { sortRows, toggleSorting } from "./sorting";
|
|
11
|
+
export { createInitialState } from "./state";
|
|
12
|
+
export { createFormatters, formatCellValue } from "./values";
|
|
13
|
+
export type { Formatters } from "./values";
|
|
14
|
+
export type * from "./types";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { GridOptions, GridRow, PaginationState, RowId } from "./types";
|
|
2
|
+
|
|
3
|
+
export type RowIdGetter<T> = (row: T, index: number) => RowId;
|
|
4
|
+
|
|
5
|
+
export function createRowIdGetter<T>(
|
|
6
|
+
getRowId: GridOptions<T>["getRowId"],
|
|
7
|
+
): RowIdGetter<T> {
|
|
8
|
+
if (typeof getRowId === "function") {
|
|
9
|
+
return (row, index) => String(getRowId(row, index));
|
|
10
|
+
}
|
|
11
|
+
if (typeof getRowId === "string") {
|
|
12
|
+
return (row) => String(row[getRowId]);
|
|
13
|
+
}
|
|
14
|
+
return (row, index) => {
|
|
15
|
+
const id = (row as { id?: unknown } | null)?.id;
|
|
16
|
+
return id === null || id === undefined ? String(index) : String(id);
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildRows<T>(
|
|
21
|
+
data: readonly T[],
|
|
22
|
+
getRowId: RowIdGetter<T>,
|
|
23
|
+
): GridRow<T>[] {
|
|
24
|
+
return data.map((original, index) => ({
|
|
25
|
+
id: getRowId(original, index),
|
|
26
|
+
index,
|
|
27
|
+
original,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface PageResult<T> {
|
|
32
|
+
rows: GridRow<T>[];
|
|
33
|
+
pageIndex: number;
|
|
34
|
+
pageCount: number;
|
|
35
|
+
/** Total rows across all pages. */
|
|
36
|
+
rowCount: number;
|
|
37
|
+
/** Absolute index of the first row on the page. */
|
|
38
|
+
pageOffset: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function paginate<T>(
|
|
42
|
+
rows: GridRow<T>[],
|
|
43
|
+
pagination: PaginationState,
|
|
44
|
+
options: { enabled: boolean; server: boolean; rowCount?: number },
|
|
45
|
+
): PageResult<T> {
|
|
46
|
+
const total = options.server ? (options.rowCount ?? rows.length) : rows.length;
|
|
47
|
+
|
|
48
|
+
if (!options.enabled) {
|
|
49
|
+
return { rows, pageIndex: 0, pageCount: 1, rowCount: total, pageOffset: 0 };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const pageSize = Math.max(1, pagination.pageSize);
|
|
53
|
+
const pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
54
|
+
const pageIndex = Math.min(Math.max(0, pagination.pageIndex), pageCount - 1);
|
|
55
|
+
const pageOffset = pageIndex * pageSize;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
rows: options.server ? rows : rows.slice(pageOffset, pageOffset + pageSize),
|
|
59
|
+
pageIndex,
|
|
60
|
+
pageCount,
|
|
61
|
+
rowCount: total,
|
|
62
|
+
pageOffset,
|
|
63
|
+
};
|
|
64
|
+
}
|