@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.
@@ -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(null);
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