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