@mui/x-data-grid-premium 9.0.0-beta.0 → 9.0.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/CHANGELOG.md +267 -1
- package/DataGridPremium/DataGridPremium.js +11 -3
- package/DataGridPremium/DataGridPremium.mjs +11 -3
- package/DataGridPremium/useDataGridPremiumProps.js +1 -0
- package/DataGridPremium/useDataGridPremiumProps.mjs +1 -0
- package/components/GridBottomContainer.js +4 -1
- package/components/GridBottomContainer.mjs +4 -1
- package/hooks/features/cellSelection/useGridCellSelection.d.mts +1 -1
- package/hooks/features/cellSelection/useGridCellSelection.d.ts +1 -1
- package/hooks/features/cellSelection/useGridCellSelection.js +863 -3
- package/hooks/features/cellSelection/useGridCellSelection.mjs +864 -4
- package/hooks/features/clipboard/useGridClipboardImport.d.mts +22 -0
- package/hooks/features/clipboard/useGridClipboardImport.d.ts +22 -0
- package/hooks/features/clipboard/useGridClipboardImport.js +7 -1
- package/hooks/features/clipboard/useGridClipboardImport.mjs +8 -3
- package/hooks/features/rows/useGridRowsOverridableMethods.js +1 -1
- package/hooks/features/rows/useGridRowsOverridableMethods.mjs +1 -1
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/models/dataGridPremiumProps.d.mts +7 -0
- package/models/dataGridPremiumProps.d.ts +7 -0
- package/package.json +9 -9
|
@@ -14,6 +14,7 @@ var _useEventCallback = _interopRequireDefault(require("@mui/utils/useEventCallb
|
|
|
14
14
|
var _internals = require("@mui/x-data-grid-pro/internals");
|
|
15
15
|
var _xDataGridPro = require("@mui/x-data-grid-pro");
|
|
16
16
|
var _gridCellSelectionSelector = require("./gridCellSelectionSelector");
|
|
17
|
+
var _useGridClipboardImport = require("../clipboard/useGridClipboardImport");
|
|
17
18
|
const cellSelectionStateInitializer = (state, props) => (0, _extends2.default)({}, state, {
|
|
18
19
|
cellSelection: (0, _extends2.default)({}, props.cellSelectionModel ?? props.initialState?.cellSelection)
|
|
19
20
|
});
|
|
@@ -23,14 +24,51 @@ function isKeyboardEvent(event) {
|
|
|
23
24
|
}
|
|
24
25
|
const AUTO_SCROLL_SENSITIVITY = 50; // The distance from the edge to start scrolling
|
|
25
26
|
const AUTO_SCROLL_SPEED = 20; // The speed to scroll once the mouse enters the sensitivity area
|
|
27
|
+
const FILL_HANDLE_HIT_AREA = 16; // px — size of the interactive hit area for the fill handle
|
|
26
28
|
|
|
29
|
+
function getSelectedOrFocusedCells(apiRef) {
|
|
30
|
+
let selectedCells = apiRef.current.getSelectedCellsAsArray();
|
|
31
|
+
if (selectedCells.length === 0) {
|
|
32
|
+
const focusedCell = (0, _xDataGridPro.gridFocusCellSelector)(apiRef);
|
|
33
|
+
if (focusedCell) {
|
|
34
|
+
selectedCells = [{
|
|
35
|
+
id: focusedCell.id,
|
|
36
|
+
field: focusedCell.field
|
|
37
|
+
}];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return selectedCells;
|
|
41
|
+
}
|
|
42
|
+
function createInitialFillDragState() {
|
|
43
|
+
return {
|
|
44
|
+
isDragging: false,
|
|
45
|
+
direction: null,
|
|
46
|
+
targetRowIds: [],
|
|
47
|
+
targetFields: [],
|
|
48
|
+
decoratedElements: new Set(),
|
|
49
|
+
moveRAF: null,
|
|
50
|
+
doc: null,
|
|
51
|
+
moveHandler: null,
|
|
52
|
+
upHandler: null
|
|
53
|
+
};
|
|
54
|
+
}
|
|
27
55
|
const useGridCellSelection = (apiRef, props) => {
|
|
28
56
|
const hasRootReference = apiRef.current.rootElementRef.current !== null;
|
|
29
57
|
const cellWithVirtualFocus = React.useRef(null);
|
|
30
58
|
const lastMouseDownCell = React.useRef(null);
|
|
31
|
-
const mousePosition = React.useRef(
|
|
59
|
+
const mousePosition = React.useRef({
|
|
60
|
+
x: 0,
|
|
61
|
+
y: 0
|
|
62
|
+
});
|
|
32
63
|
const autoScrollRAF = React.useRef(null);
|
|
33
64
|
const totalHeaderHeight = (0, _internals.getTotalHeaderHeight)(apiRef, props);
|
|
65
|
+
|
|
66
|
+
// Fill handle state — grouped by lifecycle:
|
|
67
|
+
// fillSource: set on mousedown, read-only during drag, cleared on mouseup
|
|
68
|
+
// fillDrag: managed during active drag, reset on mouseup
|
|
69
|
+
const fillSource = React.useRef(null);
|
|
70
|
+
const fillDrag = React.useRef(createInitialFillDragState());
|
|
71
|
+
const skipNextCellClick = React.useRef(false);
|
|
34
72
|
const ignoreValueFormatterProp = props.ignoreValueFormatterDuringExport;
|
|
35
73
|
const ignoreValueFormatter = (typeof ignoreValueFormatterProp === 'object' ? ignoreValueFormatterProp?.clipboardExport : ignoreValueFormatterProp) || false;
|
|
36
74
|
const clipboardCopyCellDelimiter = props.clipboardCopyCellDelimiter;
|
|
@@ -281,6 +319,12 @@ const useGridCellSelection = (apiRef, props) => {
|
|
|
281
319
|
}
|
|
282
320
|
}, [apiRef, startAutoScroll, stopAutoScroll, totalHeaderHeight]);
|
|
283
321
|
const handleCellClick = (0, _useEventCallback.default)((params, event) => {
|
|
322
|
+
// After a fill handle mousedown+mouseup (click without drag), skip the
|
|
323
|
+
// subsequent cell click so it doesn't replace the multi-cell selection.
|
|
324
|
+
if (skipNextCellClick.current) {
|
|
325
|
+
skipNextCellClick.current = false;
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
284
328
|
const {
|
|
285
329
|
id,
|
|
286
330
|
field
|
|
@@ -367,9 +411,785 @@ const useGridCellSelection = (apiRef, props) => {
|
|
|
367
411
|
field
|
|
368
412
|
}, cellWithVirtualFocus.current);
|
|
369
413
|
});
|
|
414
|
+
const serializeCellForClipboard = (0, _useEventCallback.default)((id, field) => {
|
|
415
|
+
const cellParams = apiRef.current.getCellParams(id, field);
|
|
416
|
+
return (0, _internals.serializeCellValue)(cellParams, {
|
|
417
|
+
csvOptions: {
|
|
418
|
+
delimiter: clipboardCopyCellDelimiter,
|
|
419
|
+
shouldAppendQuotes: false,
|
|
420
|
+
escapeFormulas: false
|
|
421
|
+
},
|
|
422
|
+
ignoreValueFormatter
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Helper: get source values for a specific field from stored source cells
|
|
427
|
+
const getSourceValuesForField = React.useCallback(field => {
|
|
428
|
+
const sourceValues = [];
|
|
429
|
+
for (const cell of fillSource.current?.cells ?? []) {
|
|
430
|
+
if (cell.field === field) {
|
|
431
|
+
sourceValues.push(serializeCellForClipboard(cell.id, cell.field));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return sourceValues;
|
|
435
|
+
}, [serializeCellForClipboard]);
|
|
436
|
+
const getFillSourceData = React.useCallback(() => {
|
|
437
|
+
const selectedCells = fillSource.current?.cells ?? [];
|
|
438
|
+
if (selectedCells.length === 0) {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
const visibleRows = (0, _internals.getVisibleRows)(apiRef).rows;
|
|
442
|
+
const visibleColumns = apiRef.current.getVisibleColumns();
|
|
443
|
+
const rowIndexLookup = new Map(visibleRows.map((row, index) => [String(row.id), index]));
|
|
444
|
+
const columnIndexLookup = new Map(visibleColumns.map((column, index) => [column.field, index]));
|
|
445
|
+
const orderedRowIds = [...new Set(selectedCells.map(cell => cell.id))].sort((a, b) => (rowIndexLookup.get(String(a)) ?? 0) - (rowIndexLookup.get(String(b)) ?? 0));
|
|
446
|
+
const orderedFields = [...new Set(selectedCells.map(cell => cell.field))].sort((a, b) => (columnIndexLookup.get(a) ?? 0) - (columnIndexLookup.get(b) ?? 0));
|
|
447
|
+
const valueLookup = new Map();
|
|
448
|
+
selectedCells.forEach(cell => {
|
|
449
|
+
const rowKey = String(cell.id);
|
|
450
|
+
let rowValues = valueLookup.get(rowKey);
|
|
451
|
+
if (!rowValues) {
|
|
452
|
+
rowValues = new Map();
|
|
453
|
+
valueLookup.set(rowKey, rowValues);
|
|
454
|
+
}
|
|
455
|
+
rowValues.set(cell.field, serializeCellForClipboard(cell.id, cell.field));
|
|
456
|
+
});
|
|
457
|
+
return orderedRowIds.map(rowId => {
|
|
458
|
+
const rowValues = valueLookup.get(String(rowId));
|
|
459
|
+
return orderedFields.map(field => rowValues?.get(field) ?? '');
|
|
460
|
+
});
|
|
461
|
+
}, [apiRef, serializeCellForClipboard]);
|
|
462
|
+
const getFillDownSourceData = React.useCallback(selectedCells => {
|
|
463
|
+
if (selectedCells.length === 0) {
|
|
464
|
+
return [];
|
|
465
|
+
}
|
|
466
|
+
const visibleRows = (0, _internals.getVisibleRows)(apiRef).rows;
|
|
467
|
+
const visibleColumns = apiRef.current.getVisibleColumns();
|
|
468
|
+
const rowIndexLookup = new Map(visibleRows.map((row, index) => [String(row.id), index]));
|
|
469
|
+
const columnIndexLookup = new Map(visibleColumns.map((column, index) => [column.field, index]));
|
|
470
|
+
const topCellByField = new Map();
|
|
471
|
+
selectedCells.forEach(cell => {
|
|
472
|
+
const rowIndex = rowIndexLookup.get(String(cell.id)) ?? Number.MAX_SAFE_INTEGER;
|
|
473
|
+
const currentTopCell = topCellByField.get(cell.field);
|
|
474
|
+
if (!currentTopCell || rowIndex < currentTopCell.rowIndex) {
|
|
475
|
+
topCellByField.set(cell.field, {
|
|
476
|
+
id: cell.id,
|
|
477
|
+
rowIndex
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
const orderedFields = [...topCellByField.keys()].sort((a, b) => (columnIndexLookup.get(a) ?? 0) - (columnIndexLookup.get(b) ?? 0));
|
|
482
|
+
return [orderedFields.map(field => {
|
|
483
|
+
const sourceCell = topCellByField.get(field);
|
|
484
|
+
return serializeCellForClipboard(sourceCell.id, field);
|
|
485
|
+
})];
|
|
486
|
+
}, [apiRef, serializeCellForClipboard]);
|
|
487
|
+
|
|
488
|
+
// Fill handle: apply fill using CellValueUpdater
|
|
489
|
+
const applyFill = React.useCallback(() => {
|
|
490
|
+
const targetRowIds = fillDrag.current.targetRowIds;
|
|
491
|
+
const targetFields = fillDrag.current.targetFields;
|
|
492
|
+
const direction = fillDrag.current.direction;
|
|
493
|
+
if (targetRowIds.length === 0 || targetFields.length === 0 || !direction) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
apiRef.current.publishEvent('clipboardPasteStart', {
|
|
497
|
+
data: getFillSourceData()
|
|
498
|
+
});
|
|
499
|
+
const cellUpdater = new _useGridClipboardImport.CellValueUpdater({
|
|
500
|
+
apiRef,
|
|
501
|
+
processRowUpdate: props.processRowUpdate,
|
|
502
|
+
onProcessRowUpdateError: props.onProcessRowUpdateError,
|
|
503
|
+
getRowId: props.getRowId
|
|
504
|
+
});
|
|
505
|
+
if (direction === 'vertical') {
|
|
506
|
+
// Each source column fills its own target rows independently
|
|
507
|
+
for (const field of targetFields) {
|
|
508
|
+
const sourceValues = getSourceValuesForField(field);
|
|
509
|
+
if (sourceValues.length === 0) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
targetRowIds.forEach((rowId, i) => {
|
|
513
|
+
const pastedCellValue = sourceValues[i % sourceValues.length];
|
|
514
|
+
cellUpdater.updateCell({
|
|
515
|
+
rowId,
|
|
516
|
+
field,
|
|
517
|
+
pastedCellValue
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
} else if (direction === 'horizontal') {
|
|
522
|
+
// Map source columns to target columns by position offset
|
|
523
|
+
const sourceFields = fillSource.current?.fields ?? [];
|
|
524
|
+
targetFields.forEach((targetField, colOffset) => {
|
|
525
|
+
const sourceField = sourceFields[colOffset % sourceFields.length];
|
|
526
|
+
if (!sourceField) {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const sourceValues = getSourceValuesForField(sourceField);
|
|
530
|
+
if (sourceValues.length === 0) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
targetRowIds.forEach((rowId, rowIdx) => {
|
|
534
|
+
const pastedCellValue = sourceValues[rowIdx % sourceValues.length];
|
|
535
|
+
cellUpdater.updateCell({
|
|
536
|
+
rowId,
|
|
537
|
+
field: targetField,
|
|
538
|
+
pastedCellValue
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
cellUpdater.applyUpdates();
|
|
544
|
+
|
|
545
|
+
// Extend cell selection to include filled cells
|
|
546
|
+
const currentModel = apiRef.current.getCellSelectionModel();
|
|
547
|
+
const newModel = (0, _extends2.default)({}, currentModel);
|
|
548
|
+
targetRowIds.forEach(rowId => {
|
|
549
|
+
if (!newModel[rowId]) {
|
|
550
|
+
newModel[rowId] = {};
|
|
551
|
+
}
|
|
552
|
+
targetFields.forEach(field => {
|
|
553
|
+
newModel[rowId][field] = true;
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
apiRef.current.setCellSelectionModel(newModel);
|
|
557
|
+
}, [apiRef, props.processRowUpdate, props.onProcessRowUpdateError, props.getRowId, getFillSourceData, getSourceValuesForField]);
|
|
558
|
+
|
|
559
|
+
// Helper: clear fill preview classes from previously decorated elements
|
|
560
|
+
const clearFillPreviewClasses = React.useCallback(() => {
|
|
561
|
+
const previewClasses = [_xDataGridPro.gridClasses['cell--fillPreview'], _xDataGridPro.gridClasses['cell--fillPreviewTop'], _xDataGridPro.gridClasses['cell--fillPreviewBottom'], _xDataGridPro.gridClasses['cell--fillPreviewLeft'], _xDataGridPro.gridClasses['cell--fillPreviewRight']];
|
|
562
|
+
for (const el of fillDrag.current.decoratedElements) {
|
|
563
|
+
el.classList.remove(...previewClasses);
|
|
564
|
+
}
|
|
565
|
+
fillDrag.current.decoratedElements.clear();
|
|
566
|
+
}, []);
|
|
567
|
+
|
|
568
|
+
// Helper: clean up fill drag state (used on mouseup and unmount)
|
|
569
|
+
const cleanupFillDrag = React.useCallback(() => {
|
|
570
|
+
if (fillDrag.current.moveRAF != null) {
|
|
571
|
+
cancelAnimationFrame(fillDrag.current.moveRAF);
|
|
572
|
+
}
|
|
573
|
+
const doc = fillDrag.current.doc;
|
|
574
|
+
if (doc) {
|
|
575
|
+
if (fillDrag.current.moveHandler) {
|
|
576
|
+
doc.removeEventListener('mousemove', fillDrag.current.moveHandler);
|
|
577
|
+
}
|
|
578
|
+
if (fillDrag.current.upHandler) {
|
|
579
|
+
doc.removeEventListener('mouseup', fillDrag.current.upHandler);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
clearFillPreviewClasses();
|
|
583
|
+
|
|
584
|
+
// If actual dragging occurred, the click guard is not needed — reset it
|
|
585
|
+
// so the next click on a cell works normally.
|
|
586
|
+
if (fillDrag.current.isDragging) {
|
|
587
|
+
skipNextCellClick.current = false;
|
|
588
|
+
}
|
|
589
|
+
fillDrag.current = createInitialFillDragState();
|
|
590
|
+
fillSource.current = null;
|
|
591
|
+
apiRef.current.rootElementRef?.current?.classList.remove(_xDataGridPro.gridClasses['root--disableUserSelection']);
|
|
592
|
+
}, [apiRef, clearFillPreviewClasses]);
|
|
593
|
+
|
|
594
|
+
// Fill handle: mousedown on the fill handle
|
|
595
|
+
const handleFillHandleMouseDown = React.useCallback((params, event) => {
|
|
596
|
+
if (!props.cellSelectionFillHandle || !props.cellSelection) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check if the click is on the fill handle (::after pseudo-element at bottom-right)
|
|
601
|
+
const rootEl = apiRef.current.rootElementRef?.current;
|
|
602
|
+
if (!rootEl) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const cellElement = apiRef.current.getCellElement(params.id, params.field);
|
|
606
|
+
if (!cellElement || !cellElement.classList.contains(_xDataGridPro.gridClasses['cell--withFillHandle'])) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const rect = cellElement.getBoundingClientRect();
|
|
610
|
+
const clickX = event.clientX;
|
|
611
|
+
const clickY = event.clientY;
|
|
612
|
+
const isRtl = apiRef.current.state.isRtl;
|
|
613
|
+
|
|
614
|
+
// Check if click is near the inline-end bottom corner (within hit area)
|
|
615
|
+
const isNearHandle = (isRtl ? clickX <= rect.left + FILL_HANDLE_HIT_AREA : clickX >= rect.right - FILL_HANDLE_HIT_AREA) && clickY >= rect.bottom - FILL_HANDLE_HIT_AREA && clickY >= rect.top; // Ensure click is within cell bounds
|
|
616
|
+
|
|
617
|
+
if (!isNearHandle) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Prevent default cell selection behavior
|
|
622
|
+
event.preventDefault();
|
|
623
|
+
event.stopPropagation();
|
|
624
|
+
event.defaultMuiPrevented = true;
|
|
625
|
+
|
|
626
|
+
// Skip the cell click that fires after this mousedown+mouseup so it
|
|
627
|
+
// doesn't replace the multi-cell selection with a single cell.
|
|
628
|
+
skipNextCellClick.current = true;
|
|
629
|
+
|
|
630
|
+
// Store selected cells as source (fall back to focused cell if no selection)
|
|
631
|
+
const selectedCells = getSelectedOrFocusedCells(apiRef);
|
|
632
|
+
if (selectedCells.length === 0) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Compute all source fields in visible column order
|
|
637
|
+
const visibleColumns = apiRef.current.getVisibleColumns();
|
|
638
|
+
const columnFieldToIndex = new Map(visibleColumns.map((col, i) => [col.field, i]));
|
|
639
|
+
const sourceFields = [...new Set(selectedCells.map(c => c.field))];
|
|
640
|
+
sourceFields.sort((a, b) => (columnFieldToIndex.get(a) ?? 0) - (columnFieldToIndex.get(b) ?? 0));
|
|
641
|
+
|
|
642
|
+
// Pre-compute source column index range
|
|
643
|
+
const sourceColIndices = sourceFields.map(f => columnFieldToIndex.get(f) ?? 0);
|
|
644
|
+
|
|
645
|
+
// Pre-compute source row range (doesn't change during drag)
|
|
646
|
+
const sourceRowIds = [...new Set(selectedCells.map(c => c.id))];
|
|
647
|
+
const sourceRowIndices = sourceRowIds.map(id => apiRef.current.getRowIndexRelativeToVisibleRows(id));
|
|
648
|
+
|
|
649
|
+
// Build row ID lookup map for O(1) resolution during mousemove
|
|
650
|
+
const visibleRows = (0, _internals.getVisibleRows)(apiRef);
|
|
651
|
+
const idMap = new Map();
|
|
652
|
+
for (const row of visibleRows.rows) {
|
|
653
|
+
idMap.set(String(row.id), row.id);
|
|
654
|
+
}
|
|
655
|
+
fillSource.current = {
|
|
656
|
+
cells: selectedCells,
|
|
657
|
+
fields: sourceFields,
|
|
658
|
+
columnIndexRange: {
|
|
659
|
+
start: Math.min(...sourceColIndices),
|
|
660
|
+
end: Math.max(...sourceColIndices)
|
|
661
|
+
},
|
|
662
|
+
rowIndexRange: {
|
|
663
|
+
start: Math.min(...sourceRowIndices),
|
|
664
|
+
end: Math.max(...sourceRowIndices)
|
|
665
|
+
},
|
|
666
|
+
rowIdMap: idMap
|
|
667
|
+
};
|
|
668
|
+
fillDrag.current.targetFields = [];
|
|
669
|
+
fillDrag.current.targetRowIds = [];
|
|
670
|
+
fillDrag.current.direction = null;
|
|
671
|
+
rootEl.classList.add(_xDataGridPro.gridClasses['root--disableUserSelection']);
|
|
672
|
+
const doc = (0, _ownerDocument.default)(rootEl);
|
|
673
|
+
fillDrag.current.doc = doc;
|
|
674
|
+
const handleFillMouseMove = moveEvent => {
|
|
675
|
+
// Activate dragging on the first mousemove (not on mousedown) so that a
|
|
676
|
+
// click-without-drag never sets isFillDragging — which would cause
|
|
677
|
+
// addClassesToCells to hide the fill handle indicator.
|
|
678
|
+
if (!fillDrag.current.isDragging) {
|
|
679
|
+
fillDrag.current.isDragging = true;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// Throttle via rAF to avoid layout thrashing
|
|
683
|
+
if (fillDrag.current.moveRAF != null) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
fillDrag.current.moveRAF = requestAnimationFrame(() => {
|
|
687
|
+
fillDrag.current.moveRAF = null;
|
|
688
|
+
if (!fillDrag.current.isDragging || !fillSource.current) {
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
const currentRootEl = apiRef.current.rootElementRef?.current;
|
|
692
|
+
const source = fillSource.current;
|
|
693
|
+
|
|
694
|
+
// Find which row and field the mouse is over
|
|
695
|
+
const elements = doc.elementsFromPoint(moveEvent.clientX, moveEvent.clientY);
|
|
696
|
+
let targetRowId = null;
|
|
697
|
+
let targetField = null;
|
|
698
|
+
for (const el of elements) {
|
|
699
|
+
const cellEl = el.closest('[data-field]');
|
|
700
|
+
if (cellEl) {
|
|
701
|
+
targetField = cellEl.getAttribute('data-field');
|
|
702
|
+
const rowEl = cellEl.closest('[data-id]');
|
|
703
|
+
if (rowEl) {
|
|
704
|
+
const idStr = rowEl.getAttribute('data-id');
|
|
705
|
+
if (idStr != null) {
|
|
706
|
+
// O(1) lookup via pre-built map
|
|
707
|
+
const resolved = source.rowIdMap.get(idStr);
|
|
708
|
+
if (resolved != null) {
|
|
709
|
+
targetRowId = resolved;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
if (targetRowId == null || targetField == null) {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
const {
|
|
720
|
+
start: minSourceRowIdx,
|
|
721
|
+
end: maxSourceRowIdx
|
|
722
|
+
} = source.rowIndexRange;
|
|
723
|
+
const {
|
|
724
|
+
start: minSourceColIdx,
|
|
725
|
+
end: maxSourceColIdx
|
|
726
|
+
} = source.columnIndexRange;
|
|
727
|
+
const currentVisibleRows = (0, _internals.getVisibleRows)(apiRef);
|
|
728
|
+
const currentVisibleColumns = apiRef.current.getVisibleColumns();
|
|
729
|
+
const targetRowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(targetRowId);
|
|
730
|
+
const targetColIndex = apiRef.current.getColumnIndex(targetField);
|
|
731
|
+
const isOutsideRowRange = targetRowIndex > maxSourceRowIdx || targetRowIndex < minSourceRowIdx;
|
|
732
|
+
const isOutsideColRange = targetColIndex > maxSourceColIdx || targetColIndex < minSourceColIdx;
|
|
733
|
+
|
|
734
|
+
// Determine fill direction and target cells
|
|
735
|
+
const newTargetRowIds = [];
|
|
736
|
+
let newTargetFields = [];
|
|
737
|
+
if (isOutsideRowRange) {
|
|
738
|
+
// Vertical fill: extend rows, keep all source columns
|
|
739
|
+
fillDrag.current.direction = 'vertical';
|
|
740
|
+
newTargetFields = source.fields;
|
|
741
|
+
if (targetRowIndex > maxSourceRowIdx) {
|
|
742
|
+
// Filling down
|
|
743
|
+
for (let i = maxSourceRowIdx + 1; i <= targetRowIndex; i += 1) {
|
|
744
|
+
if (i < currentVisibleRows.rows.length) {
|
|
745
|
+
newTargetRowIds.push(currentVisibleRows.rows[i].id);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
} else {
|
|
749
|
+
// Filling up
|
|
750
|
+
for (let i = targetRowIndex; i < minSourceRowIdx; i += 1) {
|
|
751
|
+
if (i >= 0) {
|
|
752
|
+
newTargetRowIds.push(currentVisibleRows.rows[i].id);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
} else if (isOutsideColRange) {
|
|
757
|
+
// Horizontal fill: extend columns, keep source rows
|
|
758
|
+
fillDrag.current.direction = 'horizontal';
|
|
759
|
+
const sourceRowIdSet = new Set(source.cells.map(c => String(c.id)));
|
|
760
|
+
for (let i = minSourceRowIdx; i <= maxSourceRowIdx; i += 1) {
|
|
761
|
+
if (i < currentVisibleRows.rows.length) {
|
|
762
|
+
const rowId = currentVisibleRows.rows[i].id;
|
|
763
|
+
if (sourceRowIdSet.has(String(rowId))) {
|
|
764
|
+
newTargetRowIds.push(rowId);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (targetColIndex > maxSourceColIdx) {
|
|
769
|
+
// Filling right
|
|
770
|
+
for (let i = maxSourceColIdx + 1; i <= targetColIndex; i += 1) {
|
|
771
|
+
if (i < currentVisibleColumns.length) {
|
|
772
|
+
newTargetFields.push(currentVisibleColumns[i].field);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
// Filling left
|
|
777
|
+
for (let i = targetColIndex; i < minSourceColIdx; i += 1) {
|
|
778
|
+
if (i >= 0) {
|
|
779
|
+
newTargetFields.push(currentVisibleColumns[i].field);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
// Mouse is within source range — no fill
|
|
785
|
+
fillDrag.current.direction = null;
|
|
786
|
+
}
|
|
787
|
+
fillDrag.current.targetRowIds = newTargetRowIds;
|
|
788
|
+
fillDrag.current.targetFields = newTargetFields;
|
|
789
|
+
|
|
790
|
+
// Apply fill preview classes directly to DOM for immediate visual feedback
|
|
791
|
+
if (currentRootEl) {
|
|
792
|
+
const nextDecorated = new Set();
|
|
793
|
+
newTargetRowIds.forEach((rowId, rowIdx) => {
|
|
794
|
+
newTargetFields.forEach((field, colIdx) => {
|
|
795
|
+
const cellEl = (0, _internals.getGridCellElement)(currentRootEl, {
|
|
796
|
+
id: rowId,
|
|
797
|
+
field
|
|
798
|
+
});
|
|
799
|
+
if (cellEl) {
|
|
800
|
+
nextDecorated.add(cellEl);
|
|
801
|
+
cellEl.classList.add(_xDataGridPro.gridClasses['cell--fillPreview']);
|
|
802
|
+
if (rowIdx === 0) {
|
|
803
|
+
cellEl.classList.add(_xDataGridPro.gridClasses['cell--fillPreviewTop']);
|
|
804
|
+
}
|
|
805
|
+
if (rowIdx === newTargetRowIds.length - 1) {
|
|
806
|
+
cellEl.classList.add(_xDataGridPro.gridClasses['cell--fillPreviewBottom']);
|
|
807
|
+
}
|
|
808
|
+
if (colIdx === 0) {
|
|
809
|
+
cellEl.classList.add(_xDataGridPro.gridClasses['cell--fillPreviewLeft']);
|
|
810
|
+
}
|
|
811
|
+
if (colIdx === newTargetFields.length - 1) {
|
|
812
|
+
cellEl.classList.add(_xDataGridPro.gridClasses['cell--fillPreviewRight']);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
// Remove classes only from elements no longer in the target set
|
|
819
|
+
for (const el of fillDrag.current.decoratedElements) {
|
|
820
|
+
if (!nextDecorated.has(el)) {
|
|
821
|
+
el.classList.remove(_xDataGridPro.gridClasses['cell--fillPreview'], _xDataGridPro.gridClasses['cell--fillPreviewTop'], _xDataGridPro.gridClasses['cell--fillPreviewBottom'], _xDataGridPro.gridClasses['cell--fillPreviewLeft'], _xDataGridPro.gridClasses['cell--fillPreviewRight']);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
fillDrag.current.decoratedElements = nextDecorated;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Auto-scroll: trigger for both vertical and horizontal edges
|
|
828
|
+
const virtualScrollerRect = apiRef.current.virtualScrollerRef?.current?.getBoundingClientRect();
|
|
829
|
+
if (virtualScrollerRect) {
|
|
830
|
+
const dimensions = (0, _xDataGridPro.gridDimensionsSelector)(apiRef);
|
|
831
|
+
const mouseX = moveEvent.clientX - virtualScrollerRect.x;
|
|
832
|
+
const mouseY = moveEvent.clientY - virtualScrollerRect.y - totalHeaderHeight;
|
|
833
|
+
const height = dimensions.viewportOuterSize.height - totalHeaderHeight;
|
|
834
|
+
const width = dimensions.viewportOuterSize.width;
|
|
835
|
+
mousePosition.current.x = mouseX;
|
|
836
|
+
mousePosition.current.y = mouseY;
|
|
837
|
+
const isInVerticalSensitivity = mouseY <= AUTO_SCROLL_SENSITIVITY || mouseY >= height - AUTO_SCROLL_SENSITIVITY;
|
|
838
|
+
const isInHorizontalSensitivity = mouseX <= AUTO_SCROLL_SENSITIVITY || mouseX >= width - AUTO_SCROLL_SENSITIVITY;
|
|
839
|
+
if (isInVerticalSensitivity || isInHorizontalSensitivity) {
|
|
840
|
+
startAutoScroll();
|
|
841
|
+
} else {
|
|
842
|
+
stopAutoScroll();
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
};
|
|
847
|
+
const handleFillMouseUp = () => {
|
|
848
|
+
stopAutoScroll();
|
|
849
|
+
if (fillDrag.current.isDragging) {
|
|
850
|
+
applyFill();
|
|
851
|
+
}
|
|
852
|
+
cleanupFillDrag();
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
// Store refs for cleanup on unmount
|
|
856
|
+
fillDrag.current.moveHandler = handleFillMouseMove;
|
|
857
|
+
fillDrag.current.upHandler = handleFillMouseUp;
|
|
858
|
+
doc.addEventListener('mousemove', handleFillMouseMove);
|
|
859
|
+
doc.addEventListener('mouseup', handleFillMouseUp);
|
|
860
|
+
}, [apiRef, props.cellSelectionFillHandle, props.cellSelection, applyFill, cleanupFillDrag, startAutoScroll, stopAutoScroll, totalHeaderHeight]);
|
|
861
|
+
|
|
862
|
+
// Fill handle: Ctrl+D to fill down
|
|
863
|
+
const handleFillKeyDown = (0, _useEventCallback.default)((_params, event) => {
|
|
864
|
+
if (!(0, _internals.isFillDownShortcut)(event)) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const selectedCells = getSelectedOrFocusedCells(apiRef);
|
|
868
|
+
if (selectedCells.length === 0) {
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
event.preventDefault();
|
|
872
|
+
event.defaultMuiPrevented = true;
|
|
873
|
+
|
|
874
|
+
// Group selected cells by field (column)
|
|
875
|
+
const cellsByField = new Map();
|
|
876
|
+
for (const cell of selectedCells) {
|
|
877
|
+
const list = cellsByField.get(cell.field) ?? [];
|
|
878
|
+
list.push(cell);
|
|
879
|
+
cellsByField.set(cell.field, list);
|
|
880
|
+
}
|
|
881
|
+
const visibleRows = (0, _internals.getVisibleRows)(apiRef);
|
|
882
|
+
const fillDownSourceData = getFillDownSourceData(selectedCells);
|
|
883
|
+
if (selectedCells.length === 1) {
|
|
884
|
+
// Single cell selected: extend selection down by one row and fill
|
|
885
|
+
const cell = selectedCells[0];
|
|
886
|
+
const colDef = apiRef.current.getColumn(cell.field);
|
|
887
|
+
if (!colDef?.editable) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(cell.id);
|
|
891
|
+
const nextRowIndex = rowIndex + 1;
|
|
892
|
+
if (nextRowIndex >= visibleRows.rows.length) {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const nextRowId = visibleRows.rows[nextRowIndex].id;
|
|
896
|
+
const sourceValue = serializeCellForClipboard(cell.id, cell.field);
|
|
897
|
+
apiRef.current.publishEvent('clipboardPasteStart', {
|
|
898
|
+
data: fillDownSourceData
|
|
899
|
+
});
|
|
900
|
+
const cellUpdater = new _useGridClipboardImport.CellValueUpdater({
|
|
901
|
+
apiRef,
|
|
902
|
+
processRowUpdate: props.processRowUpdate,
|
|
903
|
+
onProcessRowUpdateError: props.onProcessRowUpdateError,
|
|
904
|
+
getRowId: props.getRowId
|
|
905
|
+
});
|
|
906
|
+
cellUpdater.updateCell({
|
|
907
|
+
rowId: nextRowId,
|
|
908
|
+
field: cell.field,
|
|
909
|
+
pastedCellValue: sourceValue
|
|
910
|
+
});
|
|
911
|
+
cellUpdater.applyUpdates();
|
|
912
|
+
|
|
913
|
+
// Move selection and focus to the filled cell
|
|
914
|
+
apiRef.current.setCellSelectionModel({
|
|
915
|
+
[nextRowId]: {
|
|
916
|
+
[cell.field]: true
|
|
917
|
+
}
|
|
918
|
+
});
|
|
919
|
+
const colIndex = apiRef.current.getColumnIndex(cell.field);
|
|
920
|
+
apiRef.current.scrollToIndexes({
|
|
921
|
+
rowIndex: nextRowIndex,
|
|
922
|
+
colIndex
|
|
923
|
+
});
|
|
924
|
+
apiRef.current.setCellFocus(nextRowId, cell.field);
|
|
925
|
+
cellWithVirtualFocus.current = {
|
|
926
|
+
id: nextRowId,
|
|
927
|
+
field: cell.field
|
|
928
|
+
};
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Check if this is a single-row multi-column selection
|
|
933
|
+
const isSingleRowMultiColumn = selectedCells.length > 1 && [...cellsByField.values()].every(cells => cells.length === 1);
|
|
934
|
+
if (isSingleRowMultiColumn) {
|
|
935
|
+
// All cells are in the same row — extend down by one row
|
|
936
|
+
const firstCell = selectedCells[0];
|
|
937
|
+
const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(firstCell.id);
|
|
938
|
+
const nextRowIndex = rowIndex + 1;
|
|
939
|
+
if (nextRowIndex >= visibleRows.rows.length) {
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const nextRowId = visibleRows.rows[nextRowIndex].id;
|
|
943
|
+
apiRef.current.publishEvent('clipboardPasteStart', {
|
|
944
|
+
data: fillDownSourceData
|
|
945
|
+
});
|
|
946
|
+
const cellUpdater = new _useGridClipboardImport.CellValueUpdater({
|
|
947
|
+
apiRef,
|
|
948
|
+
processRowUpdate: props.processRowUpdate,
|
|
949
|
+
onProcessRowUpdateError: props.onProcessRowUpdateError,
|
|
950
|
+
getRowId: props.getRowId
|
|
951
|
+
});
|
|
952
|
+
const newSelectionModel = {};
|
|
953
|
+
for (const [field, cells] of cellsByField) {
|
|
954
|
+
const colDef = apiRef.current.getColumn(field);
|
|
955
|
+
if (!colDef?.editable) {
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
const sourceValue = serializeCellForClipboard(cells[0].id, field);
|
|
959
|
+
cellUpdater.updateCell({
|
|
960
|
+
rowId: nextRowId,
|
|
961
|
+
field,
|
|
962
|
+
pastedCellValue: sourceValue
|
|
963
|
+
});
|
|
964
|
+
if (!newSelectionModel[nextRowId]) {
|
|
965
|
+
newSelectionModel[nextRowId] = {};
|
|
966
|
+
}
|
|
967
|
+
newSelectionModel[nextRowId][field] = true;
|
|
968
|
+
}
|
|
969
|
+
cellUpdater.applyUpdates();
|
|
970
|
+
apiRef.current.setCellSelectionModel(newSelectionModel);
|
|
971
|
+
|
|
972
|
+
// Focus first editable cell in the filled row
|
|
973
|
+
const firstEditableField = [...cellsByField.keys()].find(f => apiRef.current.getColumn(f)?.editable);
|
|
974
|
+
if (firstEditableField) {
|
|
975
|
+
const colIndex = apiRef.current.getColumnIndex(firstEditableField);
|
|
976
|
+
apiRef.current.scrollToIndexes({
|
|
977
|
+
rowIndex: nextRowIndex,
|
|
978
|
+
colIndex
|
|
979
|
+
});
|
|
980
|
+
apiRef.current.setCellFocus(nextRowId, firstEditableField);
|
|
981
|
+
cellWithVirtualFocus.current = {
|
|
982
|
+
id: nextRowId,
|
|
983
|
+
field: firstEditableField
|
|
984
|
+
};
|
|
985
|
+
}
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
let cellUpdater = null;
|
|
989
|
+
|
|
990
|
+
// Multiple cells selected: for each column, top row = source, remaining = targets
|
|
991
|
+
for (const [field, cells] of cellsByField) {
|
|
992
|
+
const colDef = apiRef.current.getColumn(field);
|
|
993
|
+
if (!colDef?.editable) {
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Sort cells by row index
|
|
998
|
+
const sortedCells = cells.map(cell => (0, _extends2.default)({}, cell, {
|
|
999
|
+
rowIndex: apiRef.current.getRowIndexRelativeToVisibleRows(cell.id)
|
|
1000
|
+
})).sort((a, b) => a.rowIndex - b.rowIndex);
|
|
1001
|
+
if (sortedCells.length < 2) {
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Top row is the source
|
|
1006
|
+
const sourceCell = sortedCells[0];
|
|
1007
|
+
const sourceValue = serializeCellForClipboard(sourceCell.id, sourceCell.field);
|
|
1008
|
+
if (!cellUpdater) {
|
|
1009
|
+
apiRef.current.publishEvent('clipboardPasteStart', {
|
|
1010
|
+
data: fillDownSourceData
|
|
1011
|
+
});
|
|
1012
|
+
cellUpdater = new _useGridClipboardImport.CellValueUpdater({
|
|
1013
|
+
apiRef,
|
|
1014
|
+
processRowUpdate: props.processRowUpdate,
|
|
1015
|
+
onProcessRowUpdateError: props.onProcessRowUpdateError,
|
|
1016
|
+
getRowId: props.getRowId
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Fill all cells below the source
|
|
1021
|
+
for (let i = 1; i < sortedCells.length; i += 1) {
|
|
1022
|
+
cellUpdater.updateCell({
|
|
1023
|
+
rowId: sortedCells[i].id,
|
|
1024
|
+
field,
|
|
1025
|
+
pastedCellValue: sourceValue
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
cellUpdater?.applyUpdates();
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
// Fill handle: Ctrl+R to fill right
|
|
1033
|
+
const handleFillRightKeyDown = (0, _useEventCallback.default)((_params, event) => {
|
|
1034
|
+
if (!(0, _internals.isFillRightShortcut)(event)) {
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const selectedCells = getSelectedOrFocusedCells(apiRef);
|
|
1038
|
+
if (selectedCells.length === 0) {
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
event.preventDefault();
|
|
1042
|
+
event.defaultMuiPrevented = true;
|
|
1043
|
+
const visibleColumns = apiRef.current.getVisibleColumns();
|
|
1044
|
+
const columnFieldToIndex = new Map(visibleColumns.map((col, i) => [col.field, i]));
|
|
1045
|
+
|
|
1046
|
+
// Group selected cells by row
|
|
1047
|
+
const cellsByRow = new Map();
|
|
1048
|
+
for (const cell of selectedCells) {
|
|
1049
|
+
const list = cellsByRow.get(cell.id) ?? [];
|
|
1050
|
+
list.push(cell);
|
|
1051
|
+
cellsByRow.set(cell.id, list);
|
|
1052
|
+
}
|
|
1053
|
+
if (selectedCells.length === 1) {
|
|
1054
|
+
// Single cell: extend selection right by one column and fill
|
|
1055
|
+
const cell = selectedCells[0];
|
|
1056
|
+
const colIndex = columnFieldToIndex.get(cell.field) ?? -1;
|
|
1057
|
+
const nextColIndex = colIndex + 1;
|
|
1058
|
+
if (nextColIndex >= visibleColumns.length) {
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const nextField = visibleColumns[nextColIndex].field;
|
|
1062
|
+
const nextColDef = apiRef.current.getColumn(nextField);
|
|
1063
|
+
if (!nextColDef?.editable) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const sourceValue = serializeCellForClipboard(cell.id, cell.field);
|
|
1067
|
+
apiRef.current.publishEvent('clipboardPasteStart', {
|
|
1068
|
+
data: [[sourceValue]]
|
|
1069
|
+
});
|
|
1070
|
+
const cellUpdater = new _useGridClipboardImport.CellValueUpdater({
|
|
1071
|
+
apiRef,
|
|
1072
|
+
processRowUpdate: props.processRowUpdate,
|
|
1073
|
+
onProcessRowUpdateError: props.onProcessRowUpdateError,
|
|
1074
|
+
getRowId: props.getRowId
|
|
1075
|
+
});
|
|
1076
|
+
cellUpdater.updateCell({
|
|
1077
|
+
rowId: cell.id,
|
|
1078
|
+
field: nextField,
|
|
1079
|
+
pastedCellValue: sourceValue
|
|
1080
|
+
});
|
|
1081
|
+
cellUpdater.applyUpdates();
|
|
1082
|
+
|
|
1083
|
+
// Move selection and focus to the filled cell
|
|
1084
|
+
apiRef.current.setCellSelectionModel({
|
|
1085
|
+
[cell.id]: {
|
|
1086
|
+
[nextField]: true
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(cell.id);
|
|
1090
|
+
apiRef.current.scrollToIndexes({
|
|
1091
|
+
rowIndex,
|
|
1092
|
+
colIndex: nextColIndex
|
|
1093
|
+
});
|
|
1094
|
+
apiRef.current.setCellFocus(cell.id, nextField);
|
|
1095
|
+
cellWithVirtualFocus.current = {
|
|
1096
|
+
id: cell.id,
|
|
1097
|
+
field: nextField
|
|
1098
|
+
};
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Check if single-column multi-row selection (extend right by one column)
|
|
1103
|
+
const isSingleColumnMultiRow = [...cellsByRow.values()].every(cells => cells.length === 1);
|
|
1104
|
+
if (isSingleColumnMultiRow) {
|
|
1105
|
+
const firstCell = selectedCells[0];
|
|
1106
|
+
const colIndex = columnFieldToIndex.get(firstCell.field) ?? -1;
|
|
1107
|
+
const nextColIndex = colIndex + 1;
|
|
1108
|
+
if (nextColIndex >= visibleColumns.length) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
const nextField = visibleColumns[nextColIndex].field;
|
|
1112
|
+
const nextColDef = apiRef.current.getColumn(nextField);
|
|
1113
|
+
if (!nextColDef?.editable) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
apiRef.current.publishEvent('clipboardPasteStart', {
|
|
1117
|
+
data: [...cellsByRow.entries()].map(([, cells]) => [serializeCellForClipboard(cells[0].id, cells[0].field)])
|
|
1118
|
+
});
|
|
1119
|
+
const cellUpdater = new _useGridClipboardImport.CellValueUpdater({
|
|
1120
|
+
apiRef,
|
|
1121
|
+
processRowUpdate: props.processRowUpdate,
|
|
1122
|
+
onProcessRowUpdateError: props.onProcessRowUpdateError,
|
|
1123
|
+
getRowId: props.getRowId
|
|
1124
|
+
});
|
|
1125
|
+
const newSelectionModel = {};
|
|
1126
|
+
for (const [rowId, cells] of cellsByRow) {
|
|
1127
|
+
const sourceValue = serializeCellForClipboard(cells[0].id, cells[0].field);
|
|
1128
|
+
cellUpdater.updateCell({
|
|
1129
|
+
rowId,
|
|
1130
|
+
field: nextField,
|
|
1131
|
+
pastedCellValue: sourceValue
|
|
1132
|
+
});
|
|
1133
|
+
if (!newSelectionModel[rowId]) {
|
|
1134
|
+
newSelectionModel[rowId] = {};
|
|
1135
|
+
}
|
|
1136
|
+
newSelectionModel[rowId][nextField] = true;
|
|
1137
|
+
}
|
|
1138
|
+
cellUpdater.applyUpdates();
|
|
1139
|
+
apiRef.current.setCellSelectionModel(newSelectionModel);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// Multiple cells per row: for each row, leftmost = source, rest = targets
|
|
1144
|
+
let cellUpdater = null;
|
|
1145
|
+
for (const [rowId, cells] of cellsByRow) {
|
|
1146
|
+
// Sort cells by column index
|
|
1147
|
+
const sortedCells = cells.map(cell => (0, _extends2.default)({}, cell, {
|
|
1148
|
+
colIndex: columnFieldToIndex.get(cell.field) ?? 0
|
|
1149
|
+
})).sort((a, b) => a.colIndex - b.colIndex);
|
|
1150
|
+
if (sortedCells.length < 2) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
const sourceCell = sortedCells[0];
|
|
1154
|
+
const sourceValue = serializeCellForClipboard(sourceCell.id, sourceCell.field);
|
|
1155
|
+
if (!cellUpdater) {
|
|
1156
|
+
apiRef.current.publishEvent('clipboardPasteStart', {
|
|
1157
|
+
data: [...cellsByRow.entries()].map(([, rowCells]) => {
|
|
1158
|
+
const sorted = rowCells.map(c => (0, _extends2.default)({}, c, {
|
|
1159
|
+
colIndex: columnFieldToIndex.get(c.field) ?? 0
|
|
1160
|
+
})).sort((a, b) => a.colIndex - b.colIndex);
|
|
1161
|
+
return [serializeCellForClipboard(sorted[0].id, sorted[0].field)];
|
|
1162
|
+
})
|
|
1163
|
+
});
|
|
1164
|
+
cellUpdater = new _useGridClipboardImport.CellValueUpdater({
|
|
1165
|
+
apiRef,
|
|
1166
|
+
processRowUpdate: props.processRowUpdate,
|
|
1167
|
+
onProcessRowUpdateError: props.onProcessRowUpdateError,
|
|
1168
|
+
getRowId: props.getRowId
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Fill all cells to the right of the source
|
|
1173
|
+
for (let i = 1; i < sortedCells.length; i += 1) {
|
|
1174
|
+
const colDef = apiRef.current.getColumn(sortedCells[i].field);
|
|
1175
|
+
if (!colDef?.editable) {
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
cellUpdater.updateCell({
|
|
1179
|
+
rowId,
|
|
1180
|
+
field: sortedCells[i].field,
|
|
1181
|
+
pastedCellValue: sourceValue
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
cellUpdater?.applyUpdates();
|
|
1186
|
+
});
|
|
1187
|
+
(0, _xDataGridPro.useGridEvent)(apiRef, 'cellMouseDown', runIfCellSelectionIsEnabled(handleFillHandleMouseDown));
|
|
370
1188
|
(0, _xDataGridPro.useGridEvent)(apiRef, 'cellClick', runIfCellSelectionIsEnabled(handleCellClick));
|
|
371
1189
|
(0, _xDataGridPro.useGridEvent)(apiRef, 'cellFocusIn', runIfCellSelectionIsEnabled(handleCellFocusIn));
|
|
372
1190
|
(0, _xDataGridPro.useGridEvent)(apiRef, 'cellKeyDown', runIfCellSelectionIsEnabled(handleCellKeyDown));
|
|
1191
|
+
(0, _xDataGridPro.useGridEvent)(apiRef, 'cellKeyDown', runIfCellSelectionIsEnabled(handleFillKeyDown));
|
|
1192
|
+
(0, _xDataGridPro.useGridEvent)(apiRef, 'cellKeyDown', runIfCellSelectionIsEnabled(handleFillRightKeyDown));
|
|
373
1193
|
(0, _xDataGridPro.useGridEvent)(apiRef, 'cellMouseDown', runIfCellSelectionIsEnabled(handleCellMouseDown));
|
|
374
1194
|
(0, _xDataGridPro.useGridEvent)(apiRef, 'cellMouseOver', runIfCellSelectionIsEnabled(handleCellMouseOver));
|
|
375
1195
|
React.useEffect(() => {
|
|
@@ -381,10 +1201,11 @@ const useGridCellSelection = (apiRef, props) => {
|
|
|
381
1201
|
const rootRef = apiRef.current.rootElementRef?.current;
|
|
382
1202
|
return () => {
|
|
383
1203
|
stopAutoScroll();
|
|
1204
|
+
cleanupFillDrag();
|
|
384
1205
|
const document = (0, _ownerDocument.default)(rootRef);
|
|
385
1206
|
document.removeEventListener('mouseup', handleMouseUp);
|
|
386
1207
|
};
|
|
387
|
-
}, [apiRef, hasRootReference, handleMouseUp, stopAutoScroll]);
|
|
1208
|
+
}, [apiRef, hasRootReference, handleMouseUp, stopAutoScroll, cleanupFillDrag]);
|
|
388
1209
|
const checkIfCellIsSelected = React.useCallback((isSelected, {
|
|
389
1210
|
id,
|
|
390
1211
|
field
|
|
@@ -396,13 +1217,34 @@ const useGridCellSelection = (apiRef, props) => {
|
|
|
396
1217
|
field
|
|
397
1218
|
}) => {
|
|
398
1219
|
const visibleRows = (0, _internals.getVisibleRows)(apiRef);
|
|
1220
|
+
|
|
1221
|
+
// Note: Fill preview classes during drag are applied via direct DOM manipulation
|
|
1222
|
+
// in handleFillMouseMove for performance. The pipe processor only handles
|
|
1223
|
+
// the fill handle indicator (cell--withFillHandle) on the selection's bottom-right cell.
|
|
1224
|
+
|
|
399
1225
|
if (!visibleRows.range || !apiRef.current.isCellSelected(id, field)) {
|
|
1226
|
+
// Show fill handle on the focused cell when no cell selection exists
|
|
1227
|
+
if (props.cellSelectionFillHandle && !fillDrag.current.isDragging) {
|
|
1228
|
+
const focusedCell = (0, _xDataGridPro.gridFocusCellSelector)(apiRef);
|
|
1229
|
+
if (focusedCell && focusedCell.id === id && focusedCell.field === field) {
|
|
1230
|
+
const selectionModel = apiRef.current.getCellSelectionModel();
|
|
1231
|
+
const hasSelection = Object.keys(selectionModel).some(rowId => Object.values(selectionModel[rowId]).some(Boolean));
|
|
1232
|
+
if (!hasSelection) {
|
|
1233
|
+
const col = apiRef.current.getColumn(field);
|
|
1234
|
+
if (col?.editable) {
|
|
1235
|
+
return [...classes, _xDataGridPro.gridClasses['cell--withFillHandle']];
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
400
1240
|
return classes;
|
|
401
1241
|
}
|
|
402
1242
|
const newClasses = [...classes];
|
|
403
1243
|
const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(id);
|
|
404
1244
|
const columnIndex = apiRef.current.getColumnIndex(field);
|
|
405
1245
|
const visibleColumns = apiRef.current.getVisibleColumns();
|
|
1246
|
+
let isBottom = false;
|
|
1247
|
+
let isRight = false;
|
|
406
1248
|
if (rowIndex > 0) {
|
|
407
1249
|
const {
|
|
408
1250
|
id: previousRowId
|
|
@@ -419,9 +1261,11 @@ const useGridCellSelection = (apiRef, props) => {
|
|
|
419
1261
|
} = visibleRows.rows[rowIndex + 1];
|
|
420
1262
|
if (!apiRef.current.isCellSelected(nextRowId, field)) {
|
|
421
1263
|
newClasses.push(_xDataGridPro.gridClasses['cell--rangeBottom']);
|
|
1264
|
+
isBottom = true;
|
|
422
1265
|
}
|
|
423
1266
|
} else {
|
|
424
1267
|
newClasses.push(_xDataGridPro.gridClasses['cell--rangeBottom']);
|
|
1268
|
+
isBottom = true;
|
|
425
1269
|
}
|
|
426
1270
|
if (columnIndex > 0) {
|
|
427
1271
|
const {
|
|
@@ -439,12 +1283,28 @@ const useGridCellSelection = (apiRef, props) => {
|
|
|
439
1283
|
} = visibleColumns[columnIndex + 1];
|
|
440
1284
|
if (!apiRef.current.isCellSelected(id, nextColumnField)) {
|
|
441
1285
|
newClasses.push(_xDataGridPro.gridClasses['cell--rangeRight']);
|
|
1286
|
+
isRight = true;
|
|
442
1287
|
}
|
|
443
1288
|
} else {
|
|
444
1289
|
newClasses.push(_xDataGridPro.gridClasses['cell--rangeRight']);
|
|
1290
|
+
isRight = true;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Add fill handle to the bottom-right cell of the selection
|
|
1294
|
+
// Show if any selected column is editable (not just the bottom-right column)
|
|
1295
|
+
if (props.cellSelectionFillHandle && isBottom && isRight && !fillDrag.current.isDragging) {
|
|
1296
|
+
const selectionModel = apiRef.current.getCellSelectionModel();
|
|
1297
|
+
const selectedFieldsInRow = selectionModel[id];
|
|
1298
|
+
const hasEditableColumn = selectedFieldsInRow && Object.keys(selectedFieldsInRow).some(f => {
|
|
1299
|
+
const col = apiRef.current.getColumn(f);
|
|
1300
|
+
return col?.editable && selectedFieldsInRow[f];
|
|
1301
|
+
});
|
|
1302
|
+
if (hasEditableColumn) {
|
|
1303
|
+
newClasses.push(_xDataGridPro.gridClasses['cell--withFillHandle']);
|
|
1304
|
+
}
|
|
445
1305
|
}
|
|
446
1306
|
return newClasses;
|
|
447
|
-
}, [apiRef]);
|
|
1307
|
+
}, [apiRef, props.cellSelectionFillHandle]);
|
|
448
1308
|
const canUpdateFocus = React.useCallback((initialValue, {
|
|
449
1309
|
event,
|
|
450
1310
|
cell
|