gp-grid-react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1169 @@
1
+ import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from "react";
2
+ import { GridCore, createClientDataSource, createClientDataSource as createClientDataSource$1, createDataSourceFromArray, createDataSourceFromArray as createDataSourceFromArray$1, createServerDataSource } from "gp-grid-core";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+
5
+ //#region src/styles.ts
6
+ const STYLE_ID = "gp-grid-styles";
7
+ const gridStyles = `
8
+ /* =============================================================================
9
+ GP Grid - CSS Variables for Theming
10
+ ============================================================================= */
11
+
12
+ .gp-grid-container {
13
+ /* Colors - Light Mode (default) */
14
+ --gp-grid-bg: #ffffff;
15
+ --gp-grid-bg-alt: #f8f9fa;
16
+ --gp-grid-text: #212529;
17
+ --gp-grid-text-secondary: #6c757d;
18
+ --gp-grid-text-muted: #adb5bd;
19
+ --gp-grid-border: #dee2e6;
20
+ --gp-grid-border-light: #e9ecef;
21
+
22
+ /* Header */
23
+ --gp-grid-header-bg: #f1f3f5;
24
+ --gp-grid-header-text: #212529;
25
+
26
+ /* Selection */
27
+ --gp-grid-primary: #228be6;
28
+ --gp-grid-primary-light: #e7f5ff;
29
+ --gp-grid-primary-border: #74c0fc;
30
+ --gp-grid-hover: #f1f3f5;
31
+
32
+ /* Filter */
33
+ --gp-grid-filter-bg: #f8f9fa;
34
+ --gp-grid-input-bg: #ffffff;
35
+ --gp-grid-input-border: #ced4da;
36
+
37
+ /* Error */
38
+ --gp-grid-error-bg: #fff5f5;
39
+ --gp-grid-error-text: #c92a2a;
40
+
41
+ /* Loading */
42
+ --gp-grid-loading-bg: rgba(255, 255, 255, 0.95);
43
+ --gp-grid-loading-text: #495057;
44
+
45
+ /* Scrollbar */
46
+ --gp-grid-scrollbar-track: #f1f3f5;
47
+ --gp-grid-scrollbar-thumb: #ced4da;
48
+ --gp-grid-scrollbar-thumb-hover: #adb5bd;
49
+ }
50
+
51
+ /* Dark Mode */
52
+ .gp-grid-container--dark {
53
+ --gp-grid-bg: #1a1b1e;
54
+ --gp-grid-bg-alt: #25262b;
55
+ --gp-grid-text: #c1c2c5;
56
+ --gp-grid-text-secondary: #909296;
57
+ --gp-grid-text-muted: #5c5f66;
58
+ --gp-grid-border: #373a40;
59
+ --gp-grid-border-light: #2c2e33;
60
+
61
+ /* Header */
62
+ --gp-grid-header-bg: #25262b;
63
+ --gp-grid-header-text: #c1c2c5;
64
+
65
+ /* Selection */
66
+ --gp-grid-primary: #339af0;
67
+ --gp-grid-primary-light: #1c3d5a;
68
+ --gp-grid-primary-border: #1c7ed6;
69
+ --gp-grid-hover: #2c2e33;
70
+
71
+ /* Filter */
72
+ --gp-grid-filter-bg: #25262b;
73
+ --gp-grid-input-bg: #1a1b1e;
74
+ --gp-grid-input-border: #373a40;
75
+
76
+ /* Error */
77
+ --gp-grid-error-bg: #2c1a1a;
78
+ --gp-grid-error-text: #ff6b6b;
79
+
80
+ /* Loading */
81
+ --gp-grid-loading-bg: rgba(26, 27, 30, 0.95);
82
+ --gp-grid-loading-text: #c1c2c5;
83
+
84
+ /* Scrollbar */
85
+ --gp-grid-scrollbar-track: #25262b;
86
+ --gp-grid-scrollbar-thumb: #373a40;
87
+ --gp-grid-scrollbar-thumb-hover: #4a4d52;
88
+ }
89
+
90
+ /* =============================================================================
91
+ GP Grid - Clean Flat Design
92
+ ============================================================================= */
93
+
94
+ /* Grid Container */
95
+ .gp-grid-container {
96
+ outline: none;
97
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
98
+ font-size: 13px;
99
+ line-height: 1.5;
100
+ color: var(--gp-grid-text);
101
+ background-color: var(--gp-grid-bg);
102
+ border: 1px solid var(--gp-grid-border);
103
+ border-radius: 6px;
104
+ }
105
+
106
+ .gp-grid-container:focus {
107
+ outline: none;
108
+ border-color: var(--gp-grid-primary);
109
+ }
110
+
111
+ /* =============================================================================
112
+ Header
113
+ ============================================================================= */
114
+
115
+ .gp-grid-header {
116
+ position: sticky;
117
+ top: 0;
118
+ left: 0;
119
+ z-index: 100;
120
+ background-color: var(--gp-grid-header-bg);
121
+ border-bottom: 1px solid var(--gp-grid-border);
122
+ }
123
+
124
+ .gp-grid-container .gp-grid-header-cell {
125
+ position: absolute;
126
+ box-sizing: border-box;
127
+ border-right: 1px solid var(--gp-grid-border);
128
+ font-weight: 600;
129
+ font-size: 12px;
130
+ text-transform: uppercase;
131
+ letter-spacing: 0.5px;
132
+ color: var(--gp-grid-header-text);
133
+ cursor: pointer;
134
+ user-select: none;
135
+ display: flex;
136
+ align-items: center;
137
+ padding: 0 12px;
138
+ background-color: transparent;
139
+ transition: background-color 0.1s ease;
140
+ }
141
+
142
+ .gp-grid-container .gp-grid-header-cell:hover {
143
+ background-color: var(--gp-grid-hover);
144
+ }
145
+
146
+ .gp-grid-container .gp-grid-header-cell:active {
147
+ background-color: var(--gp-grid-border-light);
148
+ }
149
+
150
+ .gp-grid-container .gp-grid-header-text {
151
+ flex: 1;
152
+ overflow: hidden;
153
+ text-overflow: ellipsis;
154
+ white-space: nowrap;
155
+ color: var(--gp-grid-header-text);
156
+ }
157
+
158
+ .gp-grid-sort-indicator {
159
+ margin-left: 6px;
160
+ font-size: 10px;
161
+ color: var(--gp-grid-primary);
162
+ display: flex;
163
+ align-items: center;
164
+ }
165
+
166
+ .gp-grid-sort-index {
167
+ font-size: 9px;
168
+ margin-left: 2px;
169
+ color: var(--gp-grid-text-secondary);
170
+ }
171
+
172
+ /* =============================================================================
173
+ Filter Row
174
+ ============================================================================= */
175
+
176
+ .gp-grid-filter-row {
177
+ position: sticky;
178
+ left: 0;
179
+ z-index: 99;
180
+ background-color: var(--gp-grid-filter-bg);
181
+ border-bottom: 1px solid var(--gp-grid-border);
182
+ }
183
+
184
+ .gp-grid-filter-cell {
185
+ position: absolute;
186
+ box-sizing: border-box;
187
+ border-right: 1px solid var(--gp-grid-border);
188
+ padding: 6px 8px;
189
+ display: flex;
190
+ align-items: center;
191
+ background-color: var(--gp-grid-filter-bg);
192
+ }
193
+
194
+ .gp-grid-filter-input {
195
+ width: 100%;
196
+ height: 28px;
197
+ padding: 0 10px;
198
+ font-family: inherit;
199
+ font-size: 12px;
200
+ border: 1px solid var(--gp-grid-input-border);
201
+ border-radius: 4px;
202
+ background-color: var(--gp-grid-input-bg);
203
+ color: var(--gp-grid-text);
204
+ transition: border-color 0.15s ease;
205
+ }
206
+
207
+ .gp-grid-filter-input:focus {
208
+ outline: none;
209
+ border-color: var(--gp-grid-primary);
210
+ }
211
+
212
+ .gp-grid-filter-input::placeholder {
213
+ color: var(--gp-grid-text-muted);
214
+ }
215
+
216
+ /* =============================================================================
217
+ Data Cells
218
+ ============================================================================= */
219
+
220
+ .gp-grid-row {
221
+ position: absolute;
222
+ top: 0;
223
+ left: 0;
224
+ }
225
+
226
+ .gp-grid-cell {
227
+ position: absolute;
228
+ top: 0;
229
+ box-sizing: border-box;
230
+ padding: 0 12px;
231
+ display: flex;
232
+ align-items: center;
233
+ cursor: cell;
234
+ color: var(--gp-grid-text);
235
+ border-right: 1px solid var(--gp-grid-border-light);
236
+ border-bottom: 1px solid var(--gp-grid-border-light);
237
+ background-color: var(--gp-grid-bg);
238
+ overflow: hidden;
239
+ text-overflow: ellipsis;
240
+ white-space: nowrap;
241
+ }
242
+
243
+ /* Alternating row colors */
244
+ .gp-grid-row--even .gp-grid-cell {
245
+ background-color: var(--gp-grid-bg-alt);
246
+ }
247
+
248
+ .gp-grid-cell:hover {
249
+ background-color: var(--gp-grid-hover) !important;
250
+ }
251
+
252
+ /* Active cell (focused) */
253
+ .gp-grid-cell--active {
254
+ background-color: var(--gp-grid-primary-light) !important;
255
+ border: 2px solid var(--gp-grid-primary) !important;
256
+ outline: none;
257
+ z-index: 5;
258
+ padding: 0 11px;
259
+ }
260
+
261
+ /* Selected cells (range selection) */
262
+ .gp-grid-cell--selected {
263
+ background-color: var(--gp-grid-primary-light) !important;
264
+ }
265
+
266
+ /* Editing cell */
267
+ .gp-grid-cell--editing {
268
+ background-color: var(--gp-grid-bg) !important;
269
+ border: 2px solid var(--gp-grid-primary) !important;
270
+ padding: 0 !important;
271
+ z-index: 10;
272
+ }
273
+
274
+ /* =============================================================================
275
+ Fill Handle (drag to fill)
276
+ ============================================================================= */
277
+
278
+ .gp-grid-fill-handle {
279
+ position: absolute;
280
+ width: 8px;
281
+ height: 8px;
282
+ background-color: var(--gp-grid-primary);
283
+ border: 2px solid var(--gp-grid-bg);
284
+ cursor: crosshair;
285
+ z-index: 100;
286
+ pointer-events: auto;
287
+ box-sizing: border-box;
288
+ border-radius: 1px;
289
+ }
290
+
291
+ .gp-grid-fill-handle:hover {
292
+ transform: scale(1.2);
293
+ }
294
+
295
+ /* Fill preview (cells being filled) */
296
+ .gp-grid-cell--fill-preview {
297
+ background-color: var(--gp-grid-primary-light) !important;
298
+ border: 1px dashed var(--gp-grid-primary) !important;
299
+ }
300
+
301
+ /* =============================================================================
302
+ Edit Input
303
+ ============================================================================= */
304
+
305
+ .gp-grid-edit-input {
306
+ width: 100%;
307
+ height: 100%;
308
+ padding: 0 11px;
309
+ font-family: inherit;
310
+ font-size: inherit;
311
+ color: var(--gp-grid-text);
312
+ border: none;
313
+ background-color: transparent;
314
+ }
315
+
316
+ .gp-grid-edit-input:focus {
317
+ outline: none;
318
+ }
319
+
320
+ /* =============================================================================
321
+ Loading & Error States
322
+ ============================================================================= */
323
+
324
+ .gp-grid-loading {
325
+ position: absolute;
326
+ top: 50%;
327
+ left: 50%;
328
+ transform: translate(-50%, -50%);
329
+ padding: 12px 20px;
330
+ background-color: var(--gp-grid-loading-bg);
331
+ color: var(--gp-grid-loading-text);
332
+ border-radius: 6px;
333
+ border: 1px solid var(--gp-grid-border);
334
+ font-weight: 500;
335
+ font-size: 13px;
336
+ z-index: 1000;
337
+ display: flex;
338
+ align-items: center;
339
+ gap: 10px;
340
+ }
341
+
342
+ .gp-grid-loading-spinner {
343
+ width: 16px;
344
+ height: 16px;
345
+ border: 2px solid var(--gp-grid-border);
346
+ border-top-color: var(--gp-grid-primary);
347
+ border-radius: 50%;
348
+ animation: gp-grid-spin 0.7s linear infinite;
349
+ }
350
+
351
+ @keyframes gp-grid-spin {
352
+ to {
353
+ transform: rotate(360deg);
354
+ }
355
+ }
356
+
357
+ .gp-grid-error {
358
+ position: absolute;
359
+ top: 50%;
360
+ left: 50%;
361
+ transform: translate(-50%, -50%);
362
+ padding: 12px 20px;
363
+ background-color: var(--gp-grid-error-bg);
364
+ color: var(--gp-grid-error-text);
365
+ border-radius: 6px;
366
+ border: 1px solid var(--gp-grid-error-text);
367
+ font-weight: 500;
368
+ font-size: 13px;
369
+ z-index: 1000;
370
+ max-width: 80%;
371
+ text-align: center;
372
+ }
373
+
374
+ /* =============================================================================
375
+ Empty State
376
+ ============================================================================= */
377
+
378
+ .gp-grid-empty {
379
+ position: absolute;
380
+ top: 50%;
381
+ left: 50%;
382
+ transform: translate(-50%, -50%);
383
+ color: var(--gp-grid-text-muted);
384
+ font-size: 14px;
385
+ text-align: center;
386
+ }
387
+
388
+ /* =============================================================================
389
+ Scrollbar Styling
390
+ ============================================================================= */
391
+
392
+ .gp-grid-container::-webkit-scrollbar {
393
+ width: 8px;
394
+ height: 8px;
395
+ }
396
+
397
+ .gp-grid-container::-webkit-scrollbar-track {
398
+ background-color: var(--gp-grid-scrollbar-track);
399
+ }
400
+
401
+ .gp-grid-container::-webkit-scrollbar-thumb {
402
+ background-color: var(--gp-grid-scrollbar-thumb);
403
+ border-radius: 4px;
404
+ }
405
+
406
+ .gp-grid-container::-webkit-scrollbar-thumb:hover {
407
+ background-color: var(--gp-grid-scrollbar-thumb-hover);
408
+ }
409
+
410
+ .gp-grid-container::-webkit-scrollbar-corner {
411
+ background-color: var(--gp-grid-scrollbar-track);
412
+ }
413
+ `;
414
+ let stylesInjected = false;
415
+ /**
416
+ * Inject grid styles into the document head.
417
+ * This is called automatically when the Grid component mounts.
418
+ * Styles are only injected once, even if multiple Grid instances exist.
419
+ */
420
+ function injectStyles() {
421
+ if (stylesInjected) return;
422
+ if (typeof document === "undefined") return;
423
+ if (document.getElementById(STYLE_ID)) {
424
+ stylesInjected = true;
425
+ return;
426
+ }
427
+ const styleElement = document.createElement("style");
428
+ styleElement.id = STYLE_ID;
429
+ styleElement.textContent = gridStyles;
430
+ document.head.appendChild(styleElement);
431
+ stylesInjected = true;
432
+ }
433
+
434
+ //#endregion
435
+ //#region src/Grid.tsx
436
+ /**
437
+ * Apply a single instruction to mutable slot maps and return other state changes.
438
+ * This allows batching multiple slot operations efficiently.
439
+ */
440
+ function applyInstruction(instruction, slots, headers) {
441
+ switch (instruction.type) {
442
+ case "CREATE_SLOT":
443
+ slots.set(instruction.slotId, {
444
+ slotId: instruction.slotId,
445
+ rowIndex: -1,
446
+ rowData: {},
447
+ translateY: 0
448
+ });
449
+ return null;
450
+ case "DESTROY_SLOT":
451
+ slots.delete(instruction.slotId);
452
+ return null;
453
+ case "ASSIGN_SLOT": {
454
+ const existing = slots.get(instruction.slotId);
455
+ if (existing) slots.set(instruction.slotId, {
456
+ ...existing,
457
+ rowIndex: instruction.rowIndex,
458
+ rowData: instruction.rowData
459
+ });
460
+ return null;
461
+ }
462
+ case "MOVE_SLOT": {
463
+ const existing = slots.get(instruction.slotId);
464
+ if (existing) slots.set(instruction.slotId, {
465
+ ...existing,
466
+ translateY: instruction.translateY
467
+ });
468
+ return null;
469
+ }
470
+ case "SET_ACTIVE_CELL": return { activeCell: instruction.position };
471
+ case "SET_SELECTION_RANGE": return { selectionRange: instruction.range };
472
+ case "START_EDIT": return { editingCell: {
473
+ row: instruction.row,
474
+ col: instruction.col,
475
+ initialValue: instruction.initialValue
476
+ } };
477
+ case "STOP_EDIT": return { editingCell: null };
478
+ case "SET_CONTENT_SIZE": return {
479
+ contentWidth: instruction.width,
480
+ contentHeight: instruction.height
481
+ };
482
+ case "UPDATE_HEADER":
483
+ headers.set(instruction.colIndex, {
484
+ column: instruction.column,
485
+ sortDirection: instruction.sortDirection,
486
+ sortIndex: instruction.sortIndex
487
+ });
488
+ return null;
489
+ case "DATA_LOADING": return {
490
+ isLoading: true,
491
+ error: null
492
+ };
493
+ case "DATA_LOADED": return {
494
+ isLoading: false,
495
+ totalRows: instruction.totalRows
496
+ };
497
+ case "DATA_ERROR": return {
498
+ isLoading: false,
499
+ error: instruction.error
500
+ };
501
+ default: return null;
502
+ }
503
+ }
504
+ function gridReducer(state, action) {
505
+ if (action.type === "RESET") return createInitialState();
506
+ const { instructions } = action;
507
+ if (instructions.length === 0) return state;
508
+ const newSlots = new Map(state.slots);
509
+ const newHeaders = new Map(state.headers);
510
+ let stateChanges = {};
511
+ for (const instruction of instructions) {
512
+ const changes = applyInstruction(instruction, newSlots, newHeaders);
513
+ if (changes) stateChanges = {
514
+ ...stateChanges,
515
+ ...changes
516
+ };
517
+ }
518
+ return {
519
+ ...state,
520
+ ...stateChanges,
521
+ slots: newSlots,
522
+ headers: newHeaders
523
+ };
524
+ }
525
+ function createInitialState() {
526
+ return {
527
+ slots: /* @__PURE__ */ new Map(),
528
+ activeCell: null,
529
+ selectionRange: null,
530
+ editingCell: null,
531
+ contentWidth: 0,
532
+ contentHeight: 0,
533
+ headers: /* @__PURE__ */ new Map(),
534
+ isLoading: false,
535
+ error: null,
536
+ totalRows: 0
537
+ };
538
+ }
539
+ function Grid(props) {
540
+ injectStyles();
541
+ const { columns, dataSource: providedDataSource, rowData, rowHeight, headerHeight = rowHeight, overscan = 3, showFilters = false, filterDebounce = 300, darkMode = false, cellRenderers = {}, editRenderers = {}, headerRenderers = {}, cellRenderer, editRenderer, headerRenderer } = props;
542
+ const containerRef = useRef(null);
543
+ const coreRef = useRef(null);
544
+ const [state, dispatch] = useReducer(gridReducer, null, createInitialState);
545
+ const [filterValues, setFilterValues] = useState({});
546
+ const filterTimeoutRef = useRef({});
547
+ const [isDraggingFill, setIsDraggingFill] = useState(false);
548
+ const [fillTarget, setFillTarget] = useState(null);
549
+ const [fillSourceRange, setFillSourceRange] = useState(null);
550
+ const autoScrollIntervalRef = useRef(null);
551
+ const filterRowHeight = showFilters ? 40 : 0;
552
+ const totalHeaderHeight = headerHeight + filterRowHeight;
553
+ const dataSource = useMemo(() => {
554
+ if (providedDataSource) return providedDataSource;
555
+ if (rowData) return createDataSourceFromArray$1(rowData);
556
+ return createClientDataSource$1([]);
557
+ }, [providedDataSource, rowData]);
558
+ const columnPositions = useMemo(() => {
559
+ const positions = [0];
560
+ let pos = 0;
561
+ for (const col of columns) {
562
+ pos += col.width;
563
+ positions.push(pos);
564
+ }
565
+ return positions;
566
+ }, [columns]);
567
+ const totalWidth = columnPositions[columnPositions.length - 1] ?? 0;
568
+ useEffect(() => {
569
+ const core = new GridCore({
570
+ columns,
571
+ dataSource,
572
+ rowHeight,
573
+ headerHeight: totalHeaderHeight,
574
+ overscan
575
+ });
576
+ coreRef.current = core;
577
+ const unsubscribe = core.onBatchInstruction((instructions) => {
578
+ dispatch({
579
+ type: "BATCH_INSTRUCTIONS",
580
+ instructions
581
+ });
582
+ });
583
+ core.initialize();
584
+ return () => {
585
+ unsubscribe();
586
+ coreRef.current = null;
587
+ };
588
+ }, [
589
+ columns,
590
+ dataSource,
591
+ rowHeight,
592
+ totalHeaderHeight,
593
+ overscan
594
+ ]);
595
+ const handleScroll = useCallback(() => {
596
+ const container = containerRef.current;
597
+ const core = coreRef.current;
598
+ if (!container || !core) return;
599
+ core.setViewport(container.scrollTop, container.scrollLeft, container.clientWidth, container.clientHeight);
600
+ }, []);
601
+ useEffect(() => {
602
+ const container = containerRef.current;
603
+ const core = coreRef.current;
604
+ if (!container || !core) return;
605
+ const resizeObserver = new ResizeObserver(() => {
606
+ core.setViewport(container.scrollTop, container.scrollLeft, container.clientWidth, container.clientHeight);
607
+ });
608
+ resizeObserver.observe(container);
609
+ handleScroll();
610
+ return () => resizeObserver.disconnect();
611
+ }, [handleScroll]);
612
+ const handleFilterChange = useCallback((colId, value) => {
613
+ setFilterValues((prev) => ({
614
+ ...prev,
615
+ [colId]: value
616
+ }));
617
+ if (filterTimeoutRef.current[colId]) clearTimeout(filterTimeoutRef.current[colId]);
618
+ filterTimeoutRef.current[colId] = setTimeout(() => {
619
+ const core = coreRef.current;
620
+ if (core) core.setFilter(colId, value);
621
+ }, filterDebounce);
622
+ }, [filterDebounce]);
623
+ const handleKeyDown = useCallback((e) => {
624
+ const core = coreRef.current;
625
+ if (!core) return;
626
+ if (state.editingCell && e.key !== "Enter" && e.key !== "Escape" && e.key !== "Tab") return;
627
+ const { selection } = core;
628
+ const isShift = e.shiftKey;
629
+ const isCtrl = e.ctrlKey || e.metaKey;
630
+ switch (e.key) {
631
+ case "ArrowUp":
632
+ e.preventDefault();
633
+ selection.moveFocus("up", isShift);
634
+ break;
635
+ case "ArrowDown":
636
+ e.preventDefault();
637
+ selection.moveFocus("down", isShift);
638
+ break;
639
+ case "ArrowLeft":
640
+ e.preventDefault();
641
+ selection.moveFocus("left", isShift);
642
+ break;
643
+ case "ArrowRight":
644
+ e.preventDefault();
645
+ selection.moveFocus("right", isShift);
646
+ break;
647
+ case "Enter":
648
+ e.preventDefault();
649
+ if (state.editingCell) core.commitEdit();
650
+ else if (state.activeCell) core.startEdit(state.activeCell.row, state.activeCell.col);
651
+ break;
652
+ case "Escape":
653
+ e.preventDefault();
654
+ if (state.editingCell) core.cancelEdit();
655
+ else selection.clearSelection();
656
+ break;
657
+ case "Tab":
658
+ e.preventDefault();
659
+ if (state.editingCell) core.commitEdit();
660
+ selection.moveFocus(isShift ? "left" : "right", false);
661
+ break;
662
+ case "a":
663
+ if (isCtrl) {
664
+ e.preventDefault();
665
+ selection.selectAll();
666
+ }
667
+ break;
668
+ case "c":
669
+ if (isCtrl) {
670
+ e.preventDefault();
671
+ selection.copySelectionToClipboard();
672
+ }
673
+ break;
674
+ case "F2":
675
+ e.preventDefault();
676
+ if (state.activeCell && !state.editingCell) core.startEdit(state.activeCell.row, state.activeCell.col);
677
+ break;
678
+ case "Delete":
679
+ case "Backspace":
680
+ if (state.activeCell && !state.editingCell) {
681
+ e.preventDefault();
682
+ core.startEdit(state.activeCell.row, state.activeCell.col);
683
+ }
684
+ break;
685
+ default:
686
+ if (state.activeCell && !state.editingCell && !isCtrl && e.key.length === 1) core.startEdit(state.activeCell.row, state.activeCell.col);
687
+ break;
688
+ }
689
+ }, [state.activeCell, state.editingCell]);
690
+ useEffect(() => {
691
+ if (!state.activeCell || !containerRef.current || state.editingCell) return;
692
+ const { row, col } = state.activeCell;
693
+ const container = containerRef.current;
694
+ const cellTop = row * rowHeight + totalHeaderHeight;
695
+ const cellBottom = cellTop + rowHeight;
696
+ const cellLeft = columnPositions[col] ?? 0;
697
+ const cellRight = cellLeft + (columns[col]?.width ?? 0);
698
+ const visibleTop = container.scrollTop + totalHeaderHeight;
699
+ const visibleBottom = container.scrollTop + container.clientHeight;
700
+ const visibleLeft = container.scrollLeft;
701
+ const visibleRight = container.scrollLeft + container.clientWidth;
702
+ if (cellTop < visibleTop) container.scrollTop = cellTop - totalHeaderHeight;
703
+ else if (cellBottom > visibleBottom) container.scrollTop = cellBottom - container.clientHeight;
704
+ if (cellLeft < visibleLeft) container.scrollLeft = cellLeft;
705
+ else if (cellRight > visibleRight) container.scrollLeft = cellRight - container.clientWidth;
706
+ }, [
707
+ state.activeCell,
708
+ state.editingCell,
709
+ rowHeight,
710
+ totalHeaderHeight,
711
+ columnPositions,
712
+ columns
713
+ ]);
714
+ const handleCellClick = useCallback((rowIndex, colIndex, e) => {
715
+ const core = coreRef.current;
716
+ if (!core || core.getEditState() !== null) return;
717
+ containerRef.current?.focus();
718
+ core.selection.startSelection({
719
+ row: rowIndex,
720
+ col: colIndex
721
+ }, {
722
+ shift: e.shiftKey,
723
+ ctrl: e.ctrlKey || e.metaKey
724
+ });
725
+ }, []);
726
+ const handleCellDoubleClick = useCallback((rowIndex, colIndex) => {
727
+ const core = coreRef.current;
728
+ if (!core) return;
729
+ core.startEdit(rowIndex, colIndex);
730
+ }, []);
731
+ const handleHeaderClick = useCallback((colIndex, e) => {
732
+ const core = coreRef.current;
733
+ if (!core) return;
734
+ const column = columns[colIndex];
735
+ if (!column) return;
736
+ const colId = column.colId ?? column.field;
737
+ const currentDirection = state.headers.get(colIndex)?.sortDirection;
738
+ let newDirection;
739
+ if (!currentDirection) newDirection = "asc";
740
+ else if (currentDirection === "asc") newDirection = "desc";
741
+ else newDirection = null;
742
+ core.setSort(colId, newDirection, e.shiftKey);
743
+ }, [columns, state.headers]);
744
+ const handleFillHandleMouseDown = useCallback((e) => {
745
+ e.preventDefault();
746
+ e.stopPropagation();
747
+ const core = coreRef.current;
748
+ if (!core) return;
749
+ const { activeCell, selectionRange } = state;
750
+ if (!activeCell && !selectionRange) return;
751
+ const sourceRange = selectionRange ?? {
752
+ startRow: activeCell.row,
753
+ startCol: activeCell.col,
754
+ endRow: activeCell.row,
755
+ endCol: activeCell.col
756
+ };
757
+ core.fill.startFillDrag(sourceRange);
758
+ setFillSourceRange(sourceRange);
759
+ setFillTarget({
760
+ row: Math.max(sourceRange.startRow, sourceRange.endRow),
761
+ col: Math.max(sourceRange.startCol, sourceRange.endCol)
762
+ });
763
+ setIsDraggingFill(true);
764
+ }, [state.activeCell, state.selectionRange]);
765
+ useEffect(() => {
766
+ if (!isDraggingFill) return;
767
+ const SCROLL_THRESHOLD = 40;
768
+ const SCROLL_SPEED = 10;
769
+ const handleMouseMove = (e) => {
770
+ const core = coreRef.current;
771
+ const container = containerRef.current;
772
+ if (!core || !container) return;
773
+ const rect = container.getBoundingClientRect();
774
+ const scrollLeft = container.scrollLeft;
775
+ const scrollTop = container.scrollTop;
776
+ const mouseX = e.clientX - rect.left + scrollLeft;
777
+ const mouseY = e.clientY - rect.top + scrollTop - totalHeaderHeight;
778
+ const targetRow = Math.max(0, Math.floor(mouseY / rowHeight));
779
+ let targetCol = 0;
780
+ for (let i = 0; i < columnPositions.length - 1; i++) {
781
+ if (mouseX >= columnPositions[i] && mouseX < columnPositions[i + 1]) {
782
+ targetCol = i;
783
+ break;
784
+ }
785
+ if (mouseX >= columnPositions[columnPositions.length - 1]) targetCol = columnPositions.length - 2;
786
+ }
787
+ core.fill.updateFillDrag(targetRow, targetCol);
788
+ setFillTarget({
789
+ row: targetRow,
790
+ col: targetCol
791
+ });
792
+ const mouseYInContainer = e.clientY - rect.top;
793
+ const mouseXInContainer = e.clientX - rect.left;
794
+ if (autoScrollIntervalRef.current) {
795
+ clearInterval(autoScrollIntervalRef.current);
796
+ autoScrollIntervalRef.current = null;
797
+ }
798
+ let scrollDeltaX = 0;
799
+ let scrollDeltaY = 0;
800
+ if (mouseYInContainer < SCROLL_THRESHOLD + totalHeaderHeight) scrollDeltaY = -SCROLL_SPEED;
801
+ else if (mouseYInContainer > rect.height - SCROLL_THRESHOLD) scrollDeltaY = SCROLL_SPEED;
802
+ if (mouseXInContainer < SCROLL_THRESHOLD) scrollDeltaX = -SCROLL_SPEED;
803
+ else if (mouseXInContainer > rect.width - SCROLL_THRESHOLD) scrollDeltaX = SCROLL_SPEED;
804
+ if (scrollDeltaX !== 0 || scrollDeltaY !== 0) autoScrollIntervalRef.current = setInterval(() => {
805
+ if (containerRef.current) {
806
+ containerRef.current.scrollTop += scrollDeltaY;
807
+ containerRef.current.scrollLeft += scrollDeltaX;
808
+ }
809
+ }, 16);
810
+ };
811
+ const handleMouseUp = () => {
812
+ if (autoScrollIntervalRef.current) {
813
+ clearInterval(autoScrollIntervalRef.current);
814
+ autoScrollIntervalRef.current = null;
815
+ }
816
+ const core = coreRef.current;
817
+ if (core) {
818
+ core.fill.commitFillDrag();
819
+ core.refreshSlotData();
820
+ }
821
+ setIsDraggingFill(false);
822
+ setFillTarget(null);
823
+ setFillSourceRange(null);
824
+ };
825
+ document.addEventListener("mousemove", handleMouseMove);
826
+ document.addEventListener("mouseup", handleMouseUp);
827
+ return () => {
828
+ if (autoScrollIntervalRef.current) {
829
+ clearInterval(autoScrollIntervalRef.current);
830
+ autoScrollIntervalRef.current = null;
831
+ }
832
+ document.removeEventListener("mousemove", handleMouseMove);
833
+ document.removeEventListener("mouseup", handleMouseUp);
834
+ };
835
+ }, [
836
+ isDraggingFill,
837
+ totalHeaderHeight,
838
+ rowHeight,
839
+ columnPositions
840
+ ]);
841
+ const isSelected = useCallback((row, col) => {
842
+ const { selectionRange } = state;
843
+ if (!selectionRange) return false;
844
+ const minRow = Math.min(selectionRange.startRow, selectionRange.endRow);
845
+ const maxRow = Math.max(selectionRange.startRow, selectionRange.endRow);
846
+ const minCol = Math.min(selectionRange.startCol, selectionRange.endCol);
847
+ const maxCol = Math.max(selectionRange.startCol, selectionRange.endCol);
848
+ return row >= minRow && row <= maxRow && col >= minCol && col <= maxCol;
849
+ }, [state.selectionRange]);
850
+ const isActiveCell = useCallback((row, col) => {
851
+ return state.activeCell?.row === row && state.activeCell?.col === col;
852
+ }, [state.activeCell]);
853
+ const isEditingCell = useCallback((row, col) => {
854
+ return state.editingCell?.row === row && state.editingCell?.col === col;
855
+ }, [state.editingCell]);
856
+ const isInFillPreview = useCallback((row, col) => {
857
+ if (!isDraggingFill || !fillSourceRange || !fillTarget) return false;
858
+ const srcMinRow = Math.min(fillSourceRange.startRow, fillSourceRange.endRow);
859
+ const srcMaxRow = Math.max(fillSourceRange.startRow, fillSourceRange.endRow);
860
+ const srcMinCol = Math.min(fillSourceRange.startCol, fillSourceRange.endCol);
861
+ const srcMaxCol = Math.max(fillSourceRange.startCol, fillSourceRange.endCol);
862
+ const fillDown = fillTarget.row > srcMaxRow;
863
+ const fillUp = fillTarget.row < srcMinRow;
864
+ const fillRight = fillTarget.col > srcMaxCol;
865
+ const fillLeft = fillTarget.col < srcMinCol;
866
+ if (fillDown) return row > srcMaxRow && row <= fillTarget.row && col >= srcMinCol && col <= srcMaxCol;
867
+ if (fillUp) return row < srcMinRow && row >= fillTarget.row && col >= srcMinCol && col <= srcMaxCol;
868
+ if (fillRight) return col > srcMaxCol && col <= fillTarget.col && row >= srcMinRow && row <= srcMaxRow;
869
+ if (fillLeft) return col < srcMinCol && col >= fillTarget.col && row >= srcMinRow && row <= srcMaxRow;
870
+ return false;
871
+ }, [
872
+ isDraggingFill,
873
+ fillSourceRange,
874
+ fillTarget
875
+ ]);
876
+ const getCellValue = useCallback((rowData$1, field) => {
877
+ const parts = field.split(".");
878
+ let value = rowData$1;
879
+ for (const part of parts) {
880
+ if (value == null || typeof value !== "object") return null;
881
+ value = value[part];
882
+ }
883
+ return value ?? null;
884
+ }, []);
885
+ const renderCell = useCallback((column, rowData$1, rowIndex, colIndex) => {
886
+ const value = getCellValue(rowData$1, column.field);
887
+ const params = {
888
+ value,
889
+ rowData: rowData$1,
890
+ column,
891
+ rowIndex,
892
+ colIndex,
893
+ isActive: isActiveCell(rowIndex, colIndex),
894
+ isSelected: isSelected(rowIndex, colIndex),
895
+ isEditing: isEditingCell(rowIndex, colIndex)
896
+ };
897
+ if (column.cellRenderer && typeof column.cellRenderer === "string") {
898
+ const renderer = cellRenderers[column.cellRenderer];
899
+ if (renderer) return renderer(params);
900
+ }
901
+ if (cellRenderer) return cellRenderer(params);
902
+ return value == null ? "" : String(value);
903
+ }, [
904
+ getCellValue,
905
+ isActiveCell,
906
+ isSelected,
907
+ isEditingCell,
908
+ cellRenderers,
909
+ cellRenderer
910
+ ]);
911
+ const renderEditCell = useCallback((column, rowData$1, rowIndex, colIndex, initialValue) => {
912
+ const core = coreRef.current;
913
+ if (!core) return null;
914
+ const params = {
915
+ value: getCellValue(rowData$1, column.field),
916
+ rowData: rowData$1,
917
+ column,
918
+ rowIndex,
919
+ colIndex,
920
+ isActive: true,
921
+ isSelected: true,
922
+ isEditing: true,
923
+ initialValue,
924
+ onValueChange: (newValue) => core.updateEditValue(newValue),
925
+ onCommit: () => core.commitEdit(),
926
+ onCancel: () => core.cancelEdit()
927
+ };
928
+ if (column.editRenderer && typeof column.editRenderer === "string") {
929
+ const renderer = editRenderers[column.editRenderer];
930
+ if (renderer) return renderer(params);
931
+ }
932
+ if (editRenderer) return editRenderer(params);
933
+ return /* @__PURE__ */ jsx("input", {
934
+ className: "gp-grid-edit-input",
935
+ type: "text",
936
+ defaultValue: initialValue == null ? "" : String(initialValue),
937
+ autoFocus: true,
938
+ onFocus: (e) => e.target.select(),
939
+ onChange: (e) => core.updateEditValue(e.target.value),
940
+ onKeyDown: (e) => {
941
+ e.stopPropagation();
942
+ if (e.key === "Enter") core.commitEdit();
943
+ else if (e.key === "Escape") core.cancelEdit();
944
+ else if (e.key === "Tab") {
945
+ e.preventDefault();
946
+ core.commitEdit();
947
+ core.selection.moveFocus(e.shiftKey ? "left" : "right", false);
948
+ }
949
+ },
950
+ onBlur: () => core.commitEdit()
951
+ });
952
+ }, [
953
+ getCellValue,
954
+ editRenderers,
955
+ editRenderer
956
+ ]);
957
+ const renderHeader = useCallback((column, colIndex, sortDirection, sortIndex) => {
958
+ const core = coreRef.current;
959
+ const params = {
960
+ column,
961
+ colIndex,
962
+ sortDirection,
963
+ sortIndex,
964
+ onSort: (direction, addToExisting) => {
965
+ if (core) core.setSort(column.colId ?? column.field, direction, addToExisting);
966
+ }
967
+ };
968
+ if (column.headerRenderer && typeof column.headerRenderer === "string") {
969
+ const renderer = headerRenderers[column.headerRenderer];
970
+ if (renderer) return renderer(params);
971
+ }
972
+ if (headerRenderer) return headerRenderer(params);
973
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
974
+ className: "gp-grid-header-text",
975
+ children: column.headerName ?? column.field
976
+ }), sortDirection && /* @__PURE__ */ jsxs("span", {
977
+ className: "gp-grid-sort-indicator",
978
+ children: [sortDirection === "asc" ? "▲" : "▼", sortIndex !== void 0 && sortIndex > 0 && /* @__PURE__ */ jsx("span", {
979
+ className: "gp-grid-sort-index",
980
+ children: sortIndex
981
+ })]
982
+ })] });
983
+ }, [headerRenderers, headerRenderer]);
984
+ const slotsArray = useMemo(() => Array.from(state.slots.values()), [state.slots]);
985
+ const fillHandlePosition = useMemo(() => {
986
+ const { activeCell, selectionRange } = state;
987
+ if (!activeCell && !selectionRange) return null;
988
+ let row, col;
989
+ let minCol, maxCol;
990
+ if (selectionRange) {
991
+ row = Math.max(selectionRange.startRow, selectionRange.endRow);
992
+ col = Math.max(selectionRange.startCol, selectionRange.endCol);
993
+ minCol = Math.min(selectionRange.startCol, selectionRange.endCol);
994
+ maxCol = Math.max(selectionRange.startCol, selectionRange.endCol);
995
+ } else if (activeCell) {
996
+ row = activeCell.row;
997
+ col = activeCell.col;
998
+ minCol = col;
999
+ maxCol = col;
1000
+ } else return null;
1001
+ for (let c = minCol; c <= maxCol; c++) {
1002
+ const column = columns[c];
1003
+ if (!column || column.editable !== true) return null;
1004
+ }
1005
+ const cellTop = row * rowHeight + totalHeaderHeight;
1006
+ const cellLeft = columnPositions[col] ?? 0;
1007
+ const cellWidth = columns[col]?.width ?? 0;
1008
+ return {
1009
+ top: cellTop + rowHeight - 5,
1010
+ left: cellLeft + cellWidth - 20
1011
+ };
1012
+ }, [
1013
+ state.activeCell,
1014
+ state.selectionRange,
1015
+ rowHeight,
1016
+ totalHeaderHeight,
1017
+ columnPositions,
1018
+ columns
1019
+ ]);
1020
+ return /* @__PURE__ */ jsx("div", {
1021
+ ref: containerRef,
1022
+ className: `gp-grid-container${darkMode ? " gp-grid-container--dark" : ""}`,
1023
+ style: {
1024
+ width: "100%",
1025
+ height: "100%",
1026
+ overflow: "auto",
1027
+ position: "relative"
1028
+ },
1029
+ onScroll: handleScroll,
1030
+ onKeyDown: handleKeyDown,
1031
+ tabIndex: 0,
1032
+ children: /* @__PURE__ */ jsxs("div", {
1033
+ style: {
1034
+ width: Math.max(state.contentWidth, totalWidth),
1035
+ height: Math.max(state.contentHeight, totalHeaderHeight),
1036
+ position: "relative",
1037
+ minWidth: "100%"
1038
+ },
1039
+ children: [
1040
+ /* @__PURE__ */ jsx("div", {
1041
+ className: "gp-grid-header",
1042
+ style: {
1043
+ position: "sticky",
1044
+ top: 0,
1045
+ left: 0,
1046
+ height: headerHeight,
1047
+ width: Math.max(state.contentWidth, totalWidth),
1048
+ minWidth: "100%",
1049
+ zIndex: 100
1050
+ },
1051
+ children: columns.map((column, colIndex) => {
1052
+ const headerInfo = state.headers.get(colIndex);
1053
+ return /* @__PURE__ */ jsx("div", {
1054
+ className: "gp-grid-header-cell",
1055
+ style: {
1056
+ position: "absolute",
1057
+ left: `${columnPositions[colIndex]}px`,
1058
+ top: 0,
1059
+ width: `${column.width}px`,
1060
+ height: `${headerHeight}px`,
1061
+ background: "transparent"
1062
+ },
1063
+ onClick: (e) => handleHeaderClick(colIndex, e),
1064
+ children: renderHeader(column, colIndex, headerInfo?.sortDirection, headerInfo?.sortIndex)
1065
+ }, column.colId ?? column.field);
1066
+ })
1067
+ }),
1068
+ showFilters && /* @__PURE__ */ jsx("div", {
1069
+ className: "gp-grid-filter-row",
1070
+ style: {
1071
+ position: "sticky",
1072
+ top: headerHeight,
1073
+ left: 0,
1074
+ height: filterRowHeight,
1075
+ width: Math.max(state.contentWidth, totalWidth),
1076
+ minWidth: "100%",
1077
+ zIndex: 99
1078
+ },
1079
+ children: columns.map((column, colIndex) => {
1080
+ const colId = column.colId ?? column.field;
1081
+ return /* @__PURE__ */ jsx("div", {
1082
+ className: "gp-grid-filter-cell",
1083
+ style: {
1084
+ position: "absolute",
1085
+ left: `${columnPositions[colIndex]}px`,
1086
+ top: 0,
1087
+ width: `${column.width}px`,
1088
+ height: `${filterRowHeight}px`
1089
+ },
1090
+ children: /* @__PURE__ */ jsx("input", {
1091
+ className: "gp-grid-filter-input",
1092
+ type: "text",
1093
+ placeholder: `Filter ${column.headerName ?? column.field}...`,
1094
+ value: filterValues[colId] ?? "",
1095
+ onChange: (e) => handleFilterChange(colId, e.target.value),
1096
+ onKeyDown: (e) => e.stopPropagation()
1097
+ })
1098
+ }, `filter-${colId}`);
1099
+ })
1100
+ }),
1101
+ slotsArray.map((slot) => {
1102
+ if (slot.rowIndex < 0) return null;
1103
+ return /* @__PURE__ */ jsx("div", {
1104
+ className: `gp-grid-row ${slot.rowIndex % 2 === 0 ? "gp-grid-row--even" : ""}`,
1105
+ style: {
1106
+ position: "absolute",
1107
+ top: 0,
1108
+ left: 0,
1109
+ transform: `translateY(${slot.translateY}px)`,
1110
+ width: `${Math.max(state.contentWidth, totalWidth)}px`,
1111
+ height: `${rowHeight}px`
1112
+ },
1113
+ children: columns.map((column, colIndex) => {
1114
+ const isEditing = isEditingCell(slot.rowIndex, colIndex);
1115
+ const active = isActiveCell(slot.rowIndex, colIndex);
1116
+ const selected = isSelected(slot.rowIndex, colIndex);
1117
+ const inFillPreview = isInFillPreview(slot.rowIndex, colIndex);
1118
+ return /* @__PURE__ */ jsx("div", {
1119
+ className: [
1120
+ "gp-grid-cell",
1121
+ active && "gp-grid-cell--active",
1122
+ selected && !active && "gp-grid-cell--selected",
1123
+ isEditing && "gp-grid-cell--editing",
1124
+ inFillPreview && "gp-grid-cell--fill-preview"
1125
+ ].filter(Boolean).join(" "),
1126
+ style: {
1127
+ position: "absolute",
1128
+ left: `${columnPositions[colIndex]}px`,
1129
+ top: 0,
1130
+ width: `${column.width}px`,
1131
+ height: `${rowHeight}px`
1132
+ },
1133
+ onClick: (e) => handleCellClick(slot.rowIndex, colIndex, e),
1134
+ onDoubleClick: () => handleCellDoubleClick(slot.rowIndex, colIndex),
1135
+ children: isEditing && state.editingCell ? renderEditCell(column, slot.rowData, slot.rowIndex, colIndex, state.editingCell.initialValue) : renderCell(column, slot.rowData, slot.rowIndex, colIndex)
1136
+ }, `${slot.slotId}-${colIndex}`);
1137
+ })
1138
+ }, slot.slotId);
1139
+ }),
1140
+ fillHandlePosition && !state.editingCell && /* @__PURE__ */ jsx("div", {
1141
+ className: "gp-grid-fill-handle",
1142
+ style: {
1143
+ position: "absolute",
1144
+ top: fillHandlePosition.top,
1145
+ left: fillHandlePosition.left,
1146
+ zIndex: 200
1147
+ },
1148
+ onMouseDown: handleFillHandleMouseDown
1149
+ }),
1150
+ state.isLoading && /* @__PURE__ */ jsxs("div", {
1151
+ className: "gp-grid-loading",
1152
+ children: [/* @__PURE__ */ jsx("div", { className: "gp-grid-loading-spinner" }), "Loading..."]
1153
+ }),
1154
+ state.error && /* @__PURE__ */ jsxs("div", {
1155
+ className: "gp-grid-error",
1156
+ children: ["Error: ", state.error]
1157
+ }),
1158
+ !state.isLoading && !state.error && state.totalRows === 0 && /* @__PURE__ */ jsx("div", {
1159
+ className: "gp-grid-empty",
1160
+ children: "No data to display"
1161
+ })
1162
+ ]
1163
+ })
1164
+ });
1165
+ }
1166
+
1167
+ //#endregion
1168
+ export { Grid, createClientDataSource, createDataSourceFromArray, createServerDataSource };
1169
+ //# sourceMappingURL=index.js.map