@gridstorm/react 0.1.2

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/index.js ADDED
@@ -0,0 +1,1062 @@
1
+ import { createContext, memo, useContext, Component, useRef, useState, useMemo, useEffect, useCallback, useSyncExternalStore } from 'react';
2
+ import { createGrid, getValueFromData } from '@gridstorm/core';
3
+ import { DomRenderer } from '@gridstorm/dom-renderer';
4
+ import { createPortal } from 'react-dom';
5
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
+
7
+ // src/GridStorm.tsx
8
+ var GridContext = createContext(null);
9
+ function useGridContext() {
10
+ const ctx = useContext(GridContext);
11
+ if (!ctx) {
12
+ throw new Error(
13
+ "[GridStorm] Hook must be used within a <GridStorm> component."
14
+ );
15
+ }
16
+ return ctx;
17
+ }
18
+
19
+ // src/renderers/ReactCellRenderer.ts
20
+ var REACT_CELL_RENDERER = /* @__PURE__ */ Symbol.for("gridstorm:reactCellRenderer");
21
+ function reactCellRenderer(Component2) {
22
+ const fn = () => "";
23
+ fn[REACT_CELL_RENDERER] = Component2;
24
+ return fn;
25
+ }
26
+ function isReactCellRenderer(fn) {
27
+ return typeof fn === "function" && REACT_CELL_RENDERER in fn;
28
+ }
29
+ function getReactCellComponent(fn) {
30
+ if (isReactCellRenderer(fn)) {
31
+ return fn[REACT_CELL_RENDERER];
32
+ }
33
+ return null;
34
+ }
35
+
36
+ // src/renderers/ReactHeaderRenderer.ts
37
+ var REACT_HEADER_RENDERER = /* @__PURE__ */ Symbol.for("gridstorm:reactHeaderRenderer");
38
+ function reactHeaderRenderer(Component2) {
39
+ const fn = () => "";
40
+ fn[REACT_HEADER_RENDERER] = Component2;
41
+ return fn;
42
+ }
43
+ function isReactHeaderRenderer(fn) {
44
+ return typeof fn === "function" && REACT_HEADER_RENDERER in fn;
45
+ }
46
+ function getReactHeaderComponent(fn) {
47
+ if (isReactHeaderRenderer(fn)) {
48
+ return fn[REACT_HEADER_RENDERER];
49
+ }
50
+ return null;
51
+ }
52
+ function CellRendererPortalInner(props) {
53
+ const { Component: Component2, rendererProps } = props;
54
+ return /* @__PURE__ */ jsx(Component2, { ...rendererProps });
55
+ }
56
+ var CellRendererPortal = memo(
57
+ CellRendererPortalInner,
58
+ (prev, next) => prev.nodeVersion === next.nodeVersion && prev.rendererProps.value === next.rendererProps.value && prev.rendererProps.rowIndex === next.rendererProps.rowIndex && prev.rendererProps.colId === next.rendererProps.colId
59
+ );
60
+ function CellEditorPortal(props) {
61
+ const { state, api, engine, EditorComponent, editorParams, gridRootRect } = props;
62
+ const { editing, cellRect } = state;
63
+ const [value, setValue] = useState(editing.value);
64
+ const containerRef = useRef(null);
65
+ const onValueChange = useCallback(
66
+ (newValue) => {
67
+ setValue(newValue);
68
+ engine.commandBus.dispatch("editing:setValue", { value: newValue });
69
+ },
70
+ [engine]
71
+ );
72
+ const stopEditing = useCallback(
73
+ (cancel) => {
74
+ api.stopEditing(cancel);
75
+ },
76
+ [api]
77
+ );
78
+ useEffect(() => {
79
+ requestAnimationFrame(() => {
80
+ const el = containerRef.current;
81
+ if (!el) return;
82
+ const focusable = el.querySelector(
83
+ "input, textarea, select, [tabindex]"
84
+ );
85
+ focusable?.focus();
86
+ });
87
+ }, []);
88
+ const column = engine.store.getState().columns.find((c) => c.colId === editing.colId);
89
+ const node = engine.store.getState().rowNodes.get(editing.rowId);
90
+ const top = cellRect.top - gridRootRect.top;
91
+ const left = cellRect.left - gridRootRect.left;
92
+ return /* @__PURE__ */ jsx(
93
+ "div",
94
+ {
95
+ ref: containerRef,
96
+ className: "gs-editor-portal",
97
+ style: {
98
+ position: "absolute",
99
+ top,
100
+ left,
101
+ width: cellRect.width,
102
+ height: cellRect.height,
103
+ zIndex: 10,
104
+ boxSizing: "border-box",
105
+ background: "var(--gs-color-cell-editing-bg, #fff)",
106
+ border: "2px solid var(--gs-color-cell-editing-border, #2196f3)"
107
+ },
108
+ children: /* @__PURE__ */ jsx(
109
+ EditorComponent,
110
+ {
111
+ value,
112
+ data: node?.data,
113
+ colId: editing.colId,
114
+ rowId: editing.rowId,
115
+ column,
116
+ editorParams,
117
+ onValueChange,
118
+ stopEditing,
119
+ api
120
+ }
121
+ )
122
+ }
123
+ );
124
+ }
125
+ function ContextMenuPortal(props) {
126
+ const { x, y, menuProps, Component: Component2 } = props;
127
+ const ref = useRef(null);
128
+ useEffect(() => {
129
+ const handler = (e) => {
130
+ if (ref.current && !ref.current.contains(e.target)) {
131
+ menuProps.closeMenu();
132
+ }
133
+ };
134
+ const id = setTimeout(() => {
135
+ document.addEventListener("mousedown", handler);
136
+ }, 0);
137
+ return () => {
138
+ clearTimeout(id);
139
+ document.removeEventListener("mousedown", handler);
140
+ };
141
+ }, [menuProps]);
142
+ useEffect(() => {
143
+ const handler = (e) => {
144
+ if (e.key === "Escape") menuProps.closeMenu();
145
+ };
146
+ document.addEventListener("keydown", handler);
147
+ return () => document.removeEventListener("keydown", handler);
148
+ }, [menuProps]);
149
+ return /* @__PURE__ */ jsx(
150
+ "div",
151
+ {
152
+ ref,
153
+ className: "gs-context-menu-portal",
154
+ style: {
155
+ position: "absolute",
156
+ top: y,
157
+ left: x,
158
+ zIndex: 100,
159
+ minWidth: 160
160
+ },
161
+ children: /* @__PURE__ */ jsx(Component2, { ...menuProps })
162
+ }
163
+ );
164
+ }
165
+ var WRAPPER_ATTR = "data-gs-portal";
166
+ function getOrCreateWrapper(cell) {
167
+ for (let i = 0; i < cell.children.length; i++) {
168
+ const child = cell.children[i];
169
+ if (child.hasAttribute(WRAPPER_ATTR)) {
170
+ return child;
171
+ }
172
+ }
173
+ const wrapper = document.createElement("div");
174
+ wrapper.setAttribute(WRAPPER_ATTR, "");
175
+ wrapper.style.display = "contents";
176
+ cell.appendChild(wrapper);
177
+ return wrapper;
178
+ }
179
+ function PortalManager(props) {
180
+ const { engine, api, columns, rootElement, contextMenuComponent } = props;
181
+ const [cellPortals, setCellPortals] = useState(
182
+ () => /* @__PURE__ */ new Map()
183
+ );
184
+ const [headerPortals, setHeaderPortals] = useState(
185
+ () => /* @__PURE__ */ new Map()
186
+ );
187
+ const [editorPortal, setEditorPortal] = useState(null);
188
+ const [contextMenu, setContextMenu] = useState(null);
189
+ const columnsRef = useRef(columns);
190
+ columnsRef.current = columns;
191
+ const engineRef = useRef(engine);
192
+ engineRef.current = engine;
193
+ const scanningRef = useRef(false);
194
+ const reactCellRendererMap = useRef(/* @__PURE__ */ new Map());
195
+ const reactHeaderRendererMap = useRef(/* @__PURE__ */ new Map());
196
+ const reactEditorMap = useRef(/* @__PURE__ */ new Map());
197
+ useEffect(() => {
198
+ const cellMap = /* @__PURE__ */ new Map();
199
+ const headerMap = /* @__PURE__ */ new Map();
200
+ const editorMap = /* @__PURE__ */ new Map();
201
+ for (const col of columns) {
202
+ const colId = col.colId ?? col.field ?? "";
203
+ if (col.cellRenderer && isReactCellRenderer(col.cellRenderer)) {
204
+ cellMap.set(colId, getReactCellComponent(col.cellRenderer));
205
+ }
206
+ if (col.headerRenderer && isReactHeaderRenderer(col.headerRenderer)) {
207
+ headerMap.set(colId, getReactHeaderComponent(col.headerRenderer));
208
+ }
209
+ if (col.cellEditorComponent) {
210
+ editorMap.set(colId, col.cellEditorComponent);
211
+ }
212
+ }
213
+ reactCellRendererMap.current = cellMap;
214
+ reactHeaderRendererMap.current = headerMap;
215
+ reactEditorMap.current = editorMap;
216
+ }, [columns]);
217
+ const buildCellProps = useCallback(
218
+ (node, col, rowIndex) => {
219
+ const colDef = col.originalDef;
220
+ let value;
221
+ if (colDef.valueGetter) {
222
+ value = colDef.valueGetter({
223
+ data: node.data,
224
+ node,
225
+ colDef,
226
+ colId: col.colId
227
+ });
228
+ } else {
229
+ value = getValueFromData(node.data, col.field);
230
+ }
231
+ let formattedValue = value != null ? String(value) : "";
232
+ if (colDef.valueFormatter) {
233
+ formattedValue = colDef.valueFormatter({ value, data: node.data, node, colDef });
234
+ }
235
+ return {
236
+ value,
237
+ formattedValue,
238
+ data: node.data,
239
+ node,
240
+ colDef,
241
+ colId: col.colId,
242
+ rowIndex,
243
+ api
244
+ };
245
+ },
246
+ [api]
247
+ );
248
+ const scanVisibleRows = useCallback(() => {
249
+ if (!rootElement || scanningRef.current) return;
250
+ scanningRef.current = true;
251
+ try {
252
+ const bodyContainer = rootElement.querySelector(".gs-body");
253
+ if (!bodyContainer) return;
254
+ const state = engineRef.current.store.getState();
255
+ const cellRenderers = reactCellRendererMap.current;
256
+ if (cellRenderers.size === 0) return;
257
+ const newPortals = /* @__PURE__ */ new Map();
258
+ const rowElements = bodyContainer.querySelectorAll(".gs-row");
259
+ const rowIdIndexMap = /* @__PURE__ */ new Map();
260
+ for (let i = 0; i < state.displayedRowIds.length; i++) {
261
+ rowIdIndexMap.set(state.displayedRowIds[i], i);
262
+ }
263
+ const columnMap = /* @__PURE__ */ new Map();
264
+ for (const col of state.columns) {
265
+ columnMap.set(col.colId, col);
266
+ }
267
+ for (const rowEl of rowElements) {
268
+ const rowId = rowEl.getAttribute("data-row-id");
269
+ if (!rowId) continue;
270
+ const node = state.rowNodes.get(rowId);
271
+ if (!node) continue;
272
+ const cells = rowEl.querySelectorAll(".gs-cell");
273
+ for (const cellEl of cells) {
274
+ const colId = cellEl.getAttribute("data-col-id");
275
+ if (!colId || !cellRenderers.has(colId)) continue;
276
+ const key = `${rowId}:${colId}`;
277
+ const Component2 = cellRenderers.get(colId);
278
+ const colState = columnMap.get(colId);
279
+ if (!colState) continue;
280
+ const rowIndex = rowIdIndexMap.get(rowId) ?? -1;
281
+ const rendererProps = buildCellProps(node, colState, rowIndex);
282
+ const wrapper = getOrCreateWrapper(cellEl);
283
+ newPortals.set(key, {
284
+ key,
285
+ container: wrapper,
286
+ component: Component2,
287
+ props: rendererProps,
288
+ nodeVersion: node.version
289
+ });
290
+ }
291
+ }
292
+ setCellPortals(newPortals);
293
+ } finally {
294
+ scanningRef.current = false;
295
+ }
296
+ }, [rootElement, buildCellProps]);
297
+ const scanHeaderCells = useCallback(() => {
298
+ if (!rootElement) return;
299
+ const headerRenderers = reactHeaderRendererMap.current;
300
+ if (headerRenderers.size === 0) return;
301
+ const headerContainer = rootElement.querySelector(".gs-header");
302
+ if (!headerContainer) return;
303
+ const state = engineRef.current.store.getState();
304
+ const newPortals = /* @__PURE__ */ new Map();
305
+ const headerCells = headerContainer.querySelectorAll(".gs-header-cell");
306
+ for (const cellEl of headerCells) {
307
+ const colId = cellEl.getAttribute("data-col-id");
308
+ if (!colId || !headerRenderers.has(colId)) continue;
309
+ const Component2 = headerRenderers.get(colId);
310
+ const colState = state.columns.find((c) => c.colId === colId);
311
+ if (!colState) continue;
312
+ const sortItem = state.sortModel.find((s) => s.colId === colId);
313
+ const headerProps = {
314
+ colDef: colState.originalDef,
315
+ colId,
316
+ displayName: colState.headerName,
317
+ sortDirection: sortItem?.sort ?? null,
318
+ sortIndex: colState.sortIndex,
319
+ api,
320
+ onSortRequested: (multiSort) => {
321
+ engineRef.current.commandBus.dispatch("sort:toggle", { colId, multiSort });
322
+ }
323
+ };
324
+ const wrapper = getOrCreateWrapper(cellEl);
325
+ newPortals.set(colId, {
326
+ key: colId,
327
+ container: wrapper,
328
+ element: /* @__PURE__ */ jsx(Component2, { ...headerProps })
329
+ });
330
+ }
331
+ setHeaderPortals(newPortals);
332
+ }, [rootElement, api]);
333
+ useEffect(() => {
334
+ if (!rootElement) return;
335
+ const bodyContainer = rootElement.querySelector(".gs-body");
336
+ if (!bodyContainer) return;
337
+ const observer = new MutationObserver(() => {
338
+ scanVisibleRows();
339
+ });
340
+ observer.observe(bodyContainer, { childList: true });
341
+ scanVisibleRows();
342
+ scanHeaderCells();
343
+ return () => observer.disconnect();
344
+ }, [rootElement, scanVisibleRows, scanHeaderCells]);
345
+ const getVersionSnapshot = useCallback(() => engine.store.getVersion(), [engine]);
346
+ const subscribeStore = useCallback((cb) => engine.store.subscribe(cb), [engine]);
347
+ const stateVersion = useSyncExternalStore(
348
+ subscribeStore,
349
+ getVersionSnapshot,
350
+ getVersionSnapshot
351
+ );
352
+ useEffect(() => {
353
+ scanVisibleRows();
354
+ }, [stateVersion, scanVisibleRows]);
355
+ useEffect(() => {
356
+ const unsubs = [
357
+ engine.eventBus.on("column:sort:changed", () => {
358
+ requestAnimationFrame(() => scanHeaderCells());
359
+ }),
360
+ engine.eventBus.on("columns:changed", () => {
361
+ requestAnimationFrame(() => scanHeaderCells());
362
+ })
363
+ ];
364
+ return () => unsubs.forEach((u) => u());
365
+ }, [engine, scanHeaderCells]);
366
+ useEffect(() => {
367
+ const unsubStart = engine.eventBus.on(
368
+ "cell:editingStarted",
369
+ (event) => {
370
+ const { node, colId } = event;
371
+ const editorComponent = reactEditorMap.current.get(colId);
372
+ if (!editorComponent || !rootElement) return;
373
+ const cellEl = rootElement.querySelector(
374
+ `.gs-row[data-row-id="${CSS.escape(node.id)}"] .gs-cell[data-col-id="${CSS.escape(colId)}"]`
375
+ );
376
+ if (!cellEl) return;
377
+ const state = engine.store.getState();
378
+ const editing = state.editing;
379
+ if (!editing) return;
380
+ setEditorPortal({
381
+ editing,
382
+ cellElement: cellEl,
383
+ cellRect: cellEl.getBoundingClientRect()
384
+ });
385
+ }
386
+ );
387
+ const unsubStop = engine.eventBus.on("cell:editingStopped", () => {
388
+ setEditorPortal(null);
389
+ });
390
+ return () => {
391
+ unsubStart();
392
+ unsubStop();
393
+ };
394
+ }, [engine, rootElement]);
395
+ useEffect(() => {
396
+ if (!rootElement || !contextMenuComponent) return;
397
+ const handler = (e) => {
398
+ e.preventDefault();
399
+ const target = e.target;
400
+ const cellEl = target.closest(".gs-cell");
401
+ const rowEl = target.closest(".gs-row");
402
+ if (!cellEl || !rowEl) return;
403
+ const rowId = rowEl.getAttribute("data-row-id");
404
+ const colId = cellEl.getAttribute("data-col-id");
405
+ if (!rowId || !colId) return;
406
+ const state = engine.store.getState();
407
+ const node = state.rowNodes.get(rowId);
408
+ if (!node) return;
409
+ const colState = state.columns.find((c) => c.colId === colId);
410
+ const value = colState ? getValueFromData(node.data, colState.field) : void 0;
411
+ const rootRect = rootElement.getBoundingClientRect();
412
+ const rowIndex = state.displayedRowIds.indexOf(rowId);
413
+ setContextMenu({
414
+ x: e.clientX - rootRect.left,
415
+ y: e.clientY - rootRect.top,
416
+ menuProps: {
417
+ position: { rowIndex, colId },
418
+ node,
419
+ colId,
420
+ value,
421
+ api,
422
+ closeMenu: () => setContextMenu(null)
423
+ }
424
+ });
425
+ };
426
+ rootElement.addEventListener("contextmenu", handler);
427
+ return () => rootElement.removeEventListener("contextmenu", handler);
428
+ }, [rootElement, contextMenuComponent, api, engine]);
429
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
430
+ Array.from(cellPortals.values()).map(
431
+ (entry) => createPortal(
432
+ /* @__PURE__ */ jsx(
433
+ CellRendererPortal,
434
+ {
435
+ Component: entry.component,
436
+ rendererProps: entry.props,
437
+ nodeVersion: entry.nodeVersion
438
+ },
439
+ entry.key
440
+ ),
441
+ entry.container,
442
+ entry.key
443
+ )
444
+ ),
445
+ Array.from(headerPortals.values()).map(
446
+ (entry) => createPortal(entry.element, entry.container, entry.key)
447
+ ),
448
+ editorPortal && rootElement && (() => {
449
+ const editorComponent = reactEditorMap.current.get(editorPortal.editing.colId);
450
+ if (!editorComponent) return null;
451
+ const colDef = columnsRef.current.find(
452
+ (c) => (c.colId ?? c.field) === editorPortal.editing.colId
453
+ );
454
+ return createPortal(
455
+ /* @__PURE__ */ jsx(
456
+ CellEditorPortal,
457
+ {
458
+ state: editorPortal,
459
+ api,
460
+ engine,
461
+ EditorComponent: editorComponent,
462
+ editorParams: colDef?.cellEditorParams ?? {},
463
+ gridRootRect: rootElement.getBoundingClientRect()
464
+ }
465
+ ),
466
+ rootElement,
467
+ "gs-editor"
468
+ );
469
+ })(),
470
+ contextMenu && contextMenuComponent && rootElement && createPortal(
471
+ /* @__PURE__ */ jsx(
472
+ ContextMenuPortal,
473
+ {
474
+ x: contextMenu.x,
475
+ y: contextMenu.y,
476
+ menuProps: contextMenu.menuProps,
477
+ Component: contextMenuComponent
478
+ }
479
+ ),
480
+ rootElement,
481
+ "gs-context-menu"
482
+ )
483
+ ] });
484
+ }
485
+ var GridErrorBoundary = class extends Component {
486
+ constructor() {
487
+ super(...arguments);
488
+ this.state = { hasError: false, error: null };
489
+ }
490
+ static getDerivedStateFromError(error) {
491
+ return { hasError: true, error };
492
+ }
493
+ componentDidCatch(error, errorInfo) {
494
+ console.error("[GridStorm] Rendering error:", error, errorInfo.componentStack);
495
+ }
496
+ render() {
497
+ if (this.state.hasError) {
498
+ if (this.props.fallback) {
499
+ return this.props.fallback;
500
+ }
501
+ return null;
502
+ }
503
+ return this.props.children;
504
+ }
505
+ };
506
+ function useGridEngine(config) {
507
+ const [engine, setEngine] = useState(null);
508
+ const configRef = useRef(config);
509
+ configRef.current = config;
510
+ const rowDataMountedRef = useRef(false);
511
+ const columnsMountedRef = useRef(false);
512
+ useEffect(() => {
513
+ const eng = createGrid(configRef.current);
514
+ setEngine(eng);
515
+ rowDataMountedRef.current = false;
516
+ columnsMountedRef.current = false;
517
+ return () => {
518
+ eng.destroy();
519
+ setEngine(null);
520
+ };
521
+ }, []);
522
+ useEffect(() => {
523
+ if (!engine) return;
524
+ if (!rowDataMountedRef.current) {
525
+ rowDataMountedRef.current = true;
526
+ return;
527
+ }
528
+ if (config.rowData) {
529
+ engine.api.setRowData(config.rowData);
530
+ }
531
+ }, [config.rowData, engine]);
532
+ useEffect(() => {
533
+ if (!engine) return;
534
+ if (!columnsMountedRef.current) {
535
+ columnsMountedRef.current = true;
536
+ return;
537
+ }
538
+ if (config.columns) {
539
+ engine.api.setColumnDefs(config.columns);
540
+ }
541
+ }, [config.columns, engine]);
542
+ return engine;
543
+ }
544
+ function processColumns(reactColumns) {
545
+ return reactColumns.map((col) => {
546
+ const coreDef = { ...col };
547
+ delete coreDef.cellEditorComponent;
548
+ return coreDef;
549
+ });
550
+ }
551
+ function GridStorm(props) {
552
+ const {
553
+ // GridConfig props
554
+ columns: reactColumns,
555
+ rowData,
556
+ dataSource,
557
+ rowModelType,
558
+ getRowId,
559
+ plugins,
560
+ defaultColDef,
561
+ rowHeight,
562
+ headerHeight,
563
+ domLayout,
564
+ pinnedTopRowData,
565
+ pinnedBottomRowData,
566
+ suppressScrollX,
567
+ suppressScrollY,
568
+ rowSelection,
569
+ editType,
570
+ undoRedoCellEditing,
571
+ pagination,
572
+ paginationPageSize,
573
+ animateRows,
574
+ ariaLabel,
575
+ locale,
576
+ theme,
577
+ // Controlled state props
578
+ sortModel: controlledSortModel,
579
+ onSortModelChange,
580
+ filterModel: controlledFilterModel,
581
+ onFilterModelChange,
582
+ selectedRowIds: controlledSelectedRowIds,
583
+ onSelectedRowIdsChange,
584
+ currentPage: controlledCurrentPage,
585
+ onCurrentPageChange,
586
+ // Event props
587
+ onGridReady,
588
+ onRowDataChanged,
589
+ onSelectionChanged,
590
+ onSortChanged,
591
+ onFilterChanged,
592
+ onCellValueChanged,
593
+ onCellClicked,
594
+ onCellDoubleClicked,
595
+ onRowClicked,
596
+ onCellEditingStarted,
597
+ onCellEditingStopped,
598
+ onPaginationChanged,
599
+ onColumnResized,
600
+ // Renderer config props
601
+ enableCellEditing,
602
+ enableGrouping,
603
+ groupIndent,
604
+ checkboxSelection,
605
+ checkboxColumnWidth,
606
+ floatingFilter,
607
+ floatingFilterDebounce,
608
+ enablePagination,
609
+ pageSizeOptions,
610
+ // Component props
611
+ height = 400,
612
+ width = "100%",
613
+ containerClass,
614
+ containerStyle,
615
+ contextMenu,
616
+ children
617
+ // Rest are ignored (no HTML div passthrough to avoid TS errors)
618
+ } = props;
619
+ const containerRef = useRef(null);
620
+ const rendererRef = useRef(null);
621
+ const [rootElement, setRootElement] = useState(null);
622
+ const coreColumns = useMemo(
623
+ () => processColumns(reactColumns),
624
+ [reactColumns]
625
+ );
626
+ const config = useMemo(
627
+ () => ({
628
+ columns: coreColumns,
629
+ rowData,
630
+ dataSource,
631
+ rowModelType,
632
+ getRowId,
633
+ plugins,
634
+ defaultColDef,
635
+ rowHeight,
636
+ headerHeight,
637
+ domLayout,
638
+ pinnedTopRowData,
639
+ pinnedBottomRowData,
640
+ suppressScrollX,
641
+ suppressScrollY,
642
+ rowSelection,
643
+ editType,
644
+ undoRedoCellEditing,
645
+ pagination,
646
+ paginationPageSize,
647
+ animateRows,
648
+ ariaLabel,
649
+ locale,
650
+ theme
651
+ }),
652
+ // Only recreate config on structural changes
653
+ // eslint-disable-next-line react-hooks/exhaustive-deps
654
+ [coreColumns, plugins, rowModelType, getRowId]
655
+ );
656
+ const engine = useGridEngine(config);
657
+ useEffect(() => {
658
+ if (!containerRef.current || !engine) return;
659
+ const renderer = new DomRenderer({
660
+ container: containerRef.current,
661
+ engine,
662
+ enableCellEditing,
663
+ enableGrouping,
664
+ groupIndent,
665
+ checkboxSelection,
666
+ checkboxColumnWidth,
667
+ floatingFilter,
668
+ floatingFilterDebounce,
669
+ enablePagination,
670
+ pageSizeOptions
671
+ });
672
+ renderer.mount();
673
+ rendererRef.current = renderer;
674
+ const root = containerRef.current.querySelector(".gs-root");
675
+ setRootElement(root);
676
+ requestAnimationFrame(() => {
677
+ engine.eventBus.emit("grid:ready", { api: engine.api });
678
+ });
679
+ return () => {
680
+ renderer.destroy();
681
+ rendererRef.current = null;
682
+ setRootElement(null);
683
+ };
684
+ }, [engine]);
685
+ const controlledCallbacksRef = useRef({
686
+ onSortModelChange,
687
+ onFilterModelChange,
688
+ onSelectedRowIdsChange,
689
+ onCurrentPageChange
690
+ });
691
+ controlledCallbacksRef.current = {
692
+ onSortModelChange,
693
+ onFilterModelChange,
694
+ onSelectedRowIdsChange,
695
+ onCurrentPageChange
696
+ };
697
+ useEffect(() => {
698
+ if (!engine) return;
699
+ const removeMw = engine.commandBus.use((ctx) => {
700
+ const cbs = controlledCallbacksRef.current;
701
+ if (ctx.commandType === "sort:toggle" && cbs.onSortModelChange) {
702
+ return;
703
+ }
704
+ if (ctx.commandType === "selection:select" && cbs.onSelectedRowIdsChange) {
705
+ return;
706
+ }
707
+ });
708
+ return removeMw;
709
+ }, [engine]);
710
+ useEffect(() => {
711
+ if (controlledSortModel !== void 0 && engine) {
712
+ engine.api.setSortModel(controlledSortModel);
713
+ }
714
+ }, [controlledSortModel, engine]);
715
+ useEffect(() => {
716
+ if (controlledFilterModel !== void 0 && engine) {
717
+ engine.api.setFilterModel(controlledFilterModel);
718
+ }
719
+ }, [controlledFilterModel, engine]);
720
+ useEffect(() => {
721
+ if (controlledCurrentPage !== void 0 && engine) {
722
+ engine.api.paginationGoToPage(controlledCurrentPage);
723
+ }
724
+ }, [controlledCurrentPage, engine]);
725
+ useEffect(() => {
726
+ if (controlledSelectedRowIds !== void 0 && engine) {
727
+ engine.commandBus.dispatch("selection:set", {
728
+ selectedRowIds: new Set(controlledSelectedRowIds)
729
+ });
730
+ }
731
+ }, [controlledSelectedRowIds, engine]);
732
+ const eventCallbacksRef = useRef({
733
+ onGridReady,
734
+ onRowDataChanged,
735
+ onSelectionChanged,
736
+ onSortChanged,
737
+ onFilterChanged,
738
+ onCellValueChanged,
739
+ onCellClicked,
740
+ onCellDoubleClicked,
741
+ onRowClicked,
742
+ onCellEditingStarted,
743
+ onCellEditingStopped,
744
+ onPaginationChanged,
745
+ onColumnResized
746
+ });
747
+ eventCallbacksRef.current = {
748
+ onGridReady,
749
+ onRowDataChanged,
750
+ onSelectionChanged,
751
+ onSortChanged,
752
+ onFilterChanged,
753
+ onCellValueChanged,
754
+ onCellClicked,
755
+ onCellDoubleClicked,
756
+ onRowClicked,
757
+ onCellEditingStarted,
758
+ onCellEditingStopped,
759
+ onPaginationChanged,
760
+ onColumnResized
761
+ };
762
+ useEffect(() => {
763
+ if (!engine) return;
764
+ const eb = engine.eventBus;
765
+ const cbs = () => eventCallbacksRef.current;
766
+ const unsubs = [
767
+ eb.on("rowData:changed", (e) => cbs().onRowDataChanged?.(e)),
768
+ eb.on("selection:changed", (e) => {
769
+ cbs().onSelectionChanged?.(e);
770
+ controlledCallbacksRef.current.onSelectedRowIdsChange?.(
771
+ engine.store.getState().selection.selectedRowIds,
772
+ e.source ?? "api"
773
+ );
774
+ }),
775
+ eb.on("column:sort:changed", (e) => {
776
+ cbs().onSortChanged?.(e);
777
+ controlledCallbacksRef.current.onSortModelChange?.(e.sortModel);
778
+ }),
779
+ eb.on("filter:changed", (e) => {
780
+ cbs().onFilterChanged?.(e);
781
+ controlledCallbacksRef.current.onFilterModelChange?.(e.filterModel);
782
+ }),
783
+ eb.on("cell:valueChanged", (e) => cbs().onCellValueChanged?.(e)),
784
+ eb.on("cell:clicked", (e) => cbs().onCellClicked?.(e)),
785
+ eb.on("cell:doubleClicked", (e) => cbs().onCellDoubleClicked?.(e)),
786
+ eb.on("row:clicked", (e) => cbs().onRowClicked?.(e)),
787
+ eb.on("cell:editingStarted", (e) => cbs().onCellEditingStarted?.(e)),
788
+ eb.on("cell:editingStopped", (e) => cbs().onCellEditingStopped?.(e)),
789
+ eb.on("pagination:changed", (e) => {
790
+ cbs().onPaginationChanged?.(e);
791
+ controlledCallbacksRef.current.onCurrentPageChange?.(e.currentPage);
792
+ }),
793
+ eb.on("column:resized", (e) => cbs().onColumnResized?.(e))
794
+ ];
795
+ return () => unsubs.forEach((u) => u());
796
+ }, [engine]);
797
+ useEffect(() => {
798
+ if (engine) {
799
+ onGridReady?.(engine.api);
800
+ }
801
+ }, [engine]);
802
+ const style = {
803
+ height: typeof height === "number" ? `${height}px` : height,
804
+ width: typeof width === "number" ? `${width}px` : width,
805
+ ...containerStyle
806
+ };
807
+ const contextValue = useMemo(
808
+ () => engine ? { engine, api: engine.api, rootElement } : null,
809
+ [engine, rootElement]
810
+ );
811
+ if (!engine || !contextValue) {
812
+ return /* @__PURE__ */ jsx(GridErrorBoundary, { children: /* @__PURE__ */ jsx(
813
+ "div",
814
+ {
815
+ ref: containerRef,
816
+ className: `gs-container ${containerClass ?? ""}`.trim(),
817
+ style
818
+ }
819
+ ) });
820
+ }
821
+ return /* @__PURE__ */ jsx(GridErrorBoundary, { children: /* @__PURE__ */ jsxs(GridContext.Provider, { value: contextValue, children: [
822
+ children,
823
+ /* @__PURE__ */ jsx(
824
+ "div",
825
+ {
826
+ ref: containerRef,
827
+ className: `gs-container ${containerClass ?? ""}`.trim(),
828
+ style
829
+ }
830
+ ),
831
+ /* @__PURE__ */ jsx(
832
+ PortalManager,
833
+ {
834
+ engine,
835
+ api: engine.api,
836
+ columns: reactColumns,
837
+ rootElement,
838
+ contextMenuComponent: contextMenu
839
+ }
840
+ )
841
+ ] }) });
842
+ }
843
+
844
+ // src/hooks/useGridApi.ts
845
+ function useGridApi() {
846
+ return useGridContext().api;
847
+ }
848
+ function useGridState(selector) {
849
+ const { engine } = useGridContext();
850
+ const getSnapshot = useCallback(
851
+ () => selector(engine.store.getState()),
852
+ [selector, engine]
853
+ );
854
+ return useSyncExternalStore(
855
+ (onStoreChange) => engine.store.subscribe(onStoreChange),
856
+ getSnapshot,
857
+ getSnapshot
858
+ );
859
+ }
860
+ function useGridSelection() {
861
+ const { engine, api } = useGridContext();
862
+ const getSelectionSnapshot = useCallback(() => engine.store.getState().selection.selectedRowIds, [engine]);
863
+ const subscribe = useCallback((cb) => engine.store.subscribe(cb), [engine]);
864
+ const selectedRowIds = useSyncExternalStore(
865
+ subscribe,
866
+ getSelectionSnapshot,
867
+ getSelectionSnapshot
868
+ );
869
+ const selectedCount = selectedRowIds.size;
870
+ const isRowSelected = useCallback(
871
+ (rowId) => engine.store.getState().selection.selectedRowIds.has(rowId),
872
+ [engine]
873
+ );
874
+ const getSelectedRows = useCallback(() => api.getSelectedRows(), [api]);
875
+ const getSelectedNodes = useCallback(() => api.getSelectedNodes(), [api]);
876
+ const selectAll = useCallback(() => api.selectAll(), [api]);
877
+ const deselectAll = useCallback(() => api.deselectAll(), [api]);
878
+ return {
879
+ selectedRowIds,
880
+ selectedCount,
881
+ getSelectedRows,
882
+ getSelectedNodes,
883
+ isRowSelected,
884
+ selectAll,
885
+ deselectAll
886
+ };
887
+ }
888
+ function useGridSort() {
889
+ const { engine, api } = useGridContext();
890
+ const getSortSnapshot = useCallback(() => engine.store.getState().sortModel, [engine]);
891
+ const subscribe = useCallback((cb) => engine.store.subscribe(cb), [engine]);
892
+ const sortModel = useSyncExternalStore(
893
+ subscribe,
894
+ getSortSnapshot,
895
+ getSortSnapshot
896
+ );
897
+ const isSorted = sortModel.length > 0;
898
+ const setSortModel = useCallback(
899
+ (model) => api.setSortModel(model),
900
+ [api]
901
+ );
902
+ const toggleSort = useCallback(
903
+ (colId, multiSort = false) => {
904
+ engine.commandBus.dispatch("sort:toggle", { colId, multiSort });
905
+ },
906
+ [engine]
907
+ );
908
+ const clearSort = useCallback(() => api.setSortModel([]), [api]);
909
+ return { sortModel, isSorted, setSortModel, toggleSort, clearSort };
910
+ }
911
+ function useGridFilter() {
912
+ const { engine, api } = useGridContext();
913
+ const getFilterSnapshot = useCallback(() => engine.store.getState().filterModel, [engine]);
914
+ const getQuickFilterSnapshot = useCallback(() => engine.store.getState().quickFilterText, [engine]);
915
+ const subscribe = useCallback((cb) => engine.store.subscribe(cb), [engine]);
916
+ const filterModel = useSyncExternalStore(
917
+ subscribe,
918
+ getFilterSnapshot,
919
+ getFilterSnapshot
920
+ );
921
+ const quickFilterText = useSyncExternalStore(
922
+ subscribe,
923
+ getQuickFilterSnapshot,
924
+ getQuickFilterSnapshot
925
+ );
926
+ const isFiltered = Object.keys(filterModel).length > 0 || quickFilterText.length > 0;
927
+ const setFilterModel = useCallback(
928
+ (model) => api.setFilterModel(model),
929
+ [api]
930
+ );
931
+ const setQuickFilter = useCallback(
932
+ (text) => api.setQuickFilter(text),
933
+ [api]
934
+ );
935
+ const clearFilters = useCallback(() => {
936
+ api.setFilterModel({});
937
+ api.setQuickFilter("");
938
+ }, [api]);
939
+ return {
940
+ filterModel,
941
+ quickFilterText,
942
+ isFiltered,
943
+ setFilterModel,
944
+ setQuickFilter,
945
+ clearFilters
946
+ };
947
+ }
948
+ function useGridPagination() {
949
+ const { engine, api } = useGridContext();
950
+ const getPaginationSnapshot = useCallback(() => engine.store.getState().pagination, [engine]);
951
+ const subscribe = useCallback((cb) => engine.store.subscribe(cb), [engine]);
952
+ const paginationState = useSyncExternalStore(
953
+ subscribe,
954
+ getPaginationSnapshot,
955
+ getPaginationSnapshot
956
+ );
957
+ const { currentPage, pageSize, totalRows } = paginationState;
958
+ const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
959
+ const hasNextPage = currentPage < totalPages - 1;
960
+ const hasPreviousPage = currentPage > 0;
961
+ const goToPage = useCallback(
962
+ (page) => api.paginationGoToPage(page),
963
+ [api]
964
+ );
965
+ const nextPage = useCallback(() => {
966
+ if (hasNextPage) api.paginationGoToPage(currentPage + 1);
967
+ }, [api, currentPage, hasNextPage]);
968
+ const previousPage = useCallback(() => {
969
+ if (hasPreviousPage) api.paginationGoToPage(currentPage - 1);
970
+ }, [api, currentPage, hasPreviousPage]);
971
+ const firstPage = useCallback(() => api.paginationGoToPage(0), [api]);
972
+ const lastPage = useCallback(
973
+ () => api.paginationGoToPage(totalPages - 1),
974
+ [api, totalPages]
975
+ );
976
+ return useMemo(
977
+ () => ({
978
+ currentPage,
979
+ totalPages,
980
+ pageSize,
981
+ totalRows,
982
+ hasNextPage,
983
+ hasPreviousPage,
984
+ goToPage,
985
+ nextPage,
986
+ previousPage,
987
+ firstPage,
988
+ lastPage
989
+ }),
990
+ [
991
+ currentPage,
992
+ totalPages,
993
+ pageSize,
994
+ totalRows,
995
+ hasNextPage,
996
+ hasPreviousPage,
997
+ goToPage,
998
+ nextPage,
999
+ previousPage,
1000
+ firstPage,
1001
+ lastPage
1002
+ ]
1003
+ );
1004
+ }
1005
+ function useGridEvent(event, handler) {
1006
+ const { engine } = useGridContext();
1007
+ const handlerRef = useRef(handler);
1008
+ handlerRef.current = handler;
1009
+ useEffect(() => {
1010
+ const unsub = engine.eventBus.on(event, (payload) => {
1011
+ handlerRef.current(payload);
1012
+ });
1013
+ return unsub;
1014
+ }, [engine, event]);
1015
+ }
1016
+ function useGridColumn() {
1017
+ const { engine, api } = useGridContext();
1018
+ const getColumnsSnapshot = useCallback(() => engine.store.getState().columns, [engine]);
1019
+ const subscribe = useCallback((cb) => engine.store.subscribe(cb), [engine]);
1020
+ const allColumns = useSyncExternalStore(
1021
+ subscribe,
1022
+ getColumnsSnapshot,
1023
+ getColumnsSnapshot
1024
+ );
1025
+ const visibleColumns = useMemo(
1026
+ () => allColumns.filter((c) => !c.hide),
1027
+ [allColumns]
1028
+ );
1029
+ const setColumnVisible = useCallback(
1030
+ (colId, visible) => api.setColumnVisible(colId, visible),
1031
+ [api]
1032
+ );
1033
+ const setColumnWidth = useCallback(
1034
+ (colId, width) => api.setColumnWidth(colId, width),
1035
+ [api]
1036
+ );
1037
+ const moveColumn = useCallback(
1038
+ (colId, toIndex) => api.moveColumn(colId, toIndex),
1039
+ [api]
1040
+ );
1041
+ const setColumnPinned = useCallback(
1042
+ (colId, pinned) => api.setColumnPinned(colId, pinned),
1043
+ [api]
1044
+ );
1045
+ const getColumn = useCallback(
1046
+ (colId) => api.getColumn(colId),
1047
+ [api]
1048
+ );
1049
+ return {
1050
+ allColumns,
1051
+ visibleColumns,
1052
+ setColumnVisible,
1053
+ setColumnWidth,
1054
+ moveColumn,
1055
+ setColumnPinned,
1056
+ getColumn
1057
+ };
1058
+ }
1059
+
1060
+ export { GridContext, GridErrorBoundary, GridStorm, isReactCellRenderer, isReactHeaderRenderer, reactCellRenderer, reactHeaderRenderer, useGridApi, useGridColumn, useGridContext, useGridEvent, useGridFilter, useGridPagination, useGridSelection, useGridSort, useGridState };
1061
+ //# sourceMappingURL=index.js.map
1062
+ //# sourceMappingURL=index.js.map