UNPKG

@mui/x-data-grid

Version:

The community edition of the data grid component (MUI X).

461 lines (396 loc) 17.6 kB
import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray"; import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import { useGridApiEventHandler } from '../../utils/useGridApiEventHandler'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { useGridLogger } from '../../utils/useGridLogger'; import { gridRowsLookupSelector } from '../rows/gridRowsSelector'; import { gridSelectionStateSelector, selectedGridRowsSelector, selectedIdsLookupSelector } from './gridSelectionSelector'; import { gridPaginatedVisibleSortedGridRowIdsSelector } from '../pagination'; import { gridFocusCellSelector } from '../focus/gridFocusStateSelector'; import { gridVisibleSortedRowIdsSelector } from '../filter/gridFilterSelector'; import { GRID_CHECKBOX_SELECTION_COL_DEF, GRID_ACTIONS_COLUMN_TYPE } from '../../../colDef'; import { GridCellModes } from '../../../models/gridEditRowModel'; import { isKeyboardEvent, isNavigationKey } from '../../../utils/keyboardUtils'; import { getVisibleRows, useGridVisibleRows } from '../../utils/useGridVisibleRows'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD } from '../../../constants/gridDetailPanelToggleField'; var getSelectionModelPropValue = function getSelectionModelPropValue(selectionModelProp, prevSelectionModel) { if (selectionModelProp == null) { return selectionModelProp; } if (Array.isArray(selectionModelProp)) { return selectionModelProp; } if (prevSelectionModel && prevSelectionModel[0] === selectionModelProp) { return prevSelectionModel; } return [selectionModelProp]; }; export var selectionStateInitializer = function selectionStateInitializer(state, props) { var _getSelectionModelPro; return _extends({}, state, { selection: (_getSelectionModelPro = getSelectionModelPropValue(props.selectionModel)) != null ? _getSelectionModelPro : [] }); }; /** * @requires useGridRows (state, method) - can be after * @requires useGridParamsApi (method) - can be after * @requires useGridFocus (state) - can be after * @requires useGridKeyboardNavigation (`cellKeyDown` event must first be consumed by it) */ export var useGridSelection = function useGridSelection(apiRef, props) { var logger = useGridLogger(apiRef, 'useGridSelection'); var propSelectionModel = React.useMemo(function () { return getSelectionModelPropValue(props.selectionModel, gridSelectionStateSelector(apiRef.current.state)); }, [apiRef, props.selectionModel]); var lastRowToggled = React.useRef(null); apiRef.current.unstable_registerControlState({ stateId: 'selection', propModel: propSelectionModel, propOnChange: props.onSelectionModelChange, stateSelector: gridSelectionStateSelector, changeEvent: 'selectionChange' }); var checkboxSelection = props.checkboxSelection, disableMultipleSelection = props.disableMultipleSelection, disableSelectionOnClick = props.disableSelectionOnClick, pagination = props.pagination, paginationMode = props.paginationMode, propIsRowSelectable = props.isRowSelectable; var canHaveMultipleSelection = !disableMultipleSelection || checkboxSelection; var visibleRows = useGridVisibleRows(apiRef, props); var expandMouseRowRangeSelection = React.useCallback(function (id) { var _lastRowToggled$curre; var endId = id; var startId = (_lastRowToggled$curre = lastRowToggled.current) != null ? _lastRowToggled$curre : id; var isSelected = apiRef.current.isRowSelected(id); if (isSelected) { var visibleRowIds = gridVisibleSortedRowIdsSelector(apiRef); var startIndex = visibleRowIds.findIndex(function (rowId) { return rowId === startId; }); var endIndex = visibleRowIds.findIndex(function (rowId) { return rowId === endId; }); if (startIndex === endIndex) { return; } if (startIndex > endIndex) { endId = visibleRowIds[endIndex + 1]; } else { endId = visibleRowIds[endIndex - 1]; } } lastRowToggled.current = id; apiRef.current.selectRowRange({ startId: startId, endId: endId }, !isSelected); }, [apiRef]); /** * API METHODS */ var setSelectionModel = React.useCallback(function (model) { var currentModel = gridSelectionStateSelector(apiRef.current.state); if (currentModel !== model) { logger.debug("Setting selection model"); apiRef.current.setState(function (state) { return _extends({}, state, { selection: model }); }); apiRef.current.forceUpdate(); } }, [apiRef, logger]); var isRowSelected = React.useCallback(function (id) { return gridSelectionStateSelector(apiRef.current.state).includes(id); }, [apiRef]); var isRowSelectable = React.useCallback(function (id) { if (propIsRowSelectable && !propIsRowSelectable(apiRef.current.getRowParams(id))) { return false; } var rowNode = apiRef.current.getRowNode(id); if ((rowNode == null ? void 0 : rowNode.position) === 'footer' || rowNode != null && rowNode.isPinned) { return false; } return true; }, [apiRef, propIsRowSelectable]); var getSelectedRows = React.useCallback(function () { return selectedGridRowsSelector(apiRef); }, [apiRef]); var selectRow = React.useCallback(function (id) { var isSelected = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var resetSelection = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; if (!apiRef.current.isRowSelectable(id)) { return; } lastRowToggled.current = id; if (resetSelection) { logger.debug("Setting selection for row ".concat(id)); apiRef.current.setSelectionModel(isSelected ? [id] : []); } else { logger.debug("Toggling selection for row ".concat(id)); var selection = gridSelectionStateSelector(apiRef.current.state); var newSelection = selection.filter(function (el) { return el !== id; }); if (isSelected) { newSelection.push(id); } var isSelectionValid = newSelection.length < 2 || canHaveMultipleSelection; if (isSelectionValid) { apiRef.current.setSelectionModel(newSelection); } } }, [apiRef, logger, canHaveMultipleSelection]); var selectRows = React.useCallback(function (ids) { var isSelected = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var resetSelection = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; logger.debug("Setting selection for several rows"); var selectableIds = ids.filter(function (id) { return apiRef.current.isRowSelectable(id); }); var newSelection; if (resetSelection) { newSelection = isSelected ? selectableIds : []; } else { // We clone the existing object to avoid mutating the same object returned by the selector to others part of the project var selectionLookup = _extends({}, selectedIdsLookupSelector(apiRef)); selectableIds.forEach(function (id) { if (isSelected) { selectionLookup[id] = id; } else { delete selectionLookup[id]; } }); newSelection = Object.values(selectionLookup); } var isSelectionValid = newSelection.length < 2 || canHaveMultipleSelection; if (isSelectionValid) { apiRef.current.setSelectionModel(newSelection); } }, [apiRef, logger, canHaveMultipleSelection]); var selectRowRange = React.useCallback(function (_ref) { var startId = _ref.startId, endId = _ref.endId; var isSelected = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var resetSelection = arguments.length > 2 ? arguments[2] : undefined; if (!apiRef.current.getRow(startId) || !apiRef.current.getRow(endId)) { return; } logger.debug("Expanding selection from row ".concat(startId, " to row ").concat(endId)); // Using rows from all pages allow to select a range across several pages var allPagesRowIds = gridVisibleSortedRowIdsSelector(apiRef); var startIndex = allPagesRowIds.indexOf(startId); var endIndex = allPagesRowIds.indexOf(endId); var _ref2 = startIndex > endIndex ? [endIndex, startIndex] : [startIndex, endIndex], _ref3 = _slicedToArray(_ref2, 2), start = _ref3[0], end = _ref3[1]; var rowsBetweenStartAndEnd = allPagesRowIds.slice(start, end + 1); apiRef.current.selectRows(rowsBetweenStartAndEnd, isSelected, resetSelection); }, [apiRef, logger]); var selectionApi = { selectRow: selectRow, selectRows: selectRows, selectRowRange: selectRowRange, setSelectionModel: setSelectionModel, getSelectedRows: getSelectedRows, isRowSelected: isRowSelected, isRowSelectable: isRowSelectable }; useGridApiMethod(apiRef, selectionApi, 'GridSelectionApi'); /** * EVENTS */ var removeOutdatedSelection = React.useCallback(function () { if (props.keepNonExistentRowsSelected) { return; } var currentSelection = gridSelectionStateSelector(apiRef.current.state); var rowsLookup = gridRowsLookupSelector(apiRef); // We clone the existing object to avoid mutating the same object returned by the selector to others part of the project var selectionLookup = _extends({}, selectedIdsLookupSelector(apiRef)); var hasChanged = false; currentSelection.forEach(function (id) { if (!rowsLookup[id]) { delete selectionLookup[id]; hasChanged = true; } }); if (hasChanged) { apiRef.current.setSelectionModel(Object.values(selectionLookup)); } }, [apiRef, props.keepNonExistentRowsSelected]); var handleSingleRowSelection = React.useCallback(function (id, event) { var hasCtrlKey = event.metaKey || event.ctrlKey; // multiple selection is only allowed if: // - it is a checkboxSelection // - it is a keyboard selection // - Ctrl is pressed var isMultipleSelectionDisabled = !checkboxSelection && !hasCtrlKey && !isKeyboardEvent(event); var resetSelection = !canHaveMultipleSelection || isMultipleSelectionDisabled; var isSelected = apiRef.current.isRowSelected(id); if (resetSelection) { apiRef.current.selectRow(id, !isMultipleSelectionDisabled ? !isSelected : true, true); } else { apiRef.current.selectRow(id, !isSelected, false); } }, [apiRef, canHaveMultipleSelection, checkboxSelection]); var handleCellClick = React.useCallback(function (params, event) { if (disableSelectionOnClick) { return; } if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) { // click on checkbox should not trigger row selection return; } if (params.field === GRID_DETAIL_PANEL_TOGGLE_FIELD) { // click to open the detail panel should not select the row return; } if (params.field) { var column = apiRef.current.getColumn(params.field); if (column.type === GRID_ACTIONS_COLUMN_TYPE) { return; } } if (params.rowNode.isPinned) { return; } if (event.shiftKey && (canHaveMultipleSelection || checkboxSelection)) { expandMouseRowRangeSelection(params.id); } else { handleSingleRowSelection(params.id, event); } }, [disableSelectionOnClick, canHaveMultipleSelection, checkboxSelection, apiRef, expandMouseRowRangeSelection, handleSingleRowSelection]); var preventSelectionOnShift = React.useCallback(function (params, event) { if (canHaveMultipleSelection && event.shiftKey) { var _window$getSelection; (_window$getSelection = window.getSelection()) == null ? void 0 : _window$getSelection.removeAllRanges(); } }, [canHaveMultipleSelection]); var handleRowSelectionCheckboxChange = React.useCallback(function (params, event) { if (event.nativeEvent.shiftKey) { expandMouseRowRangeSelection(params.id); } else { apiRef.current.selectRow(params.id, params.value); } }, [apiRef, expandMouseRowRangeSelection]); var handleHeaderSelectionCheckboxChange = React.useCallback(function (params) { var shouldLimitSelectionToCurrentPage = props.checkboxSelectionVisibleOnly && props.pagination; var rowsToBeSelected = shouldLimitSelectionToCurrentPage ? gridPaginatedVisibleSortedGridRowIdsSelector(apiRef) : gridVisibleSortedRowIdsSelector(apiRef); apiRef.current.selectRows(rowsToBeSelected, params.value); }, [apiRef, props.checkboxSelectionVisibleOnly, props.pagination]); var handleCellKeyDown = React.useCallback(function (params, event) { // Get the most recent cell mode because it may have been changed by another listener if (apiRef.current.getCellMode(params.id, params.field) === GridCellModes.Edit) { return; } // Ignore portal // Do not apply shortcuts if the focus is not on the cell root component if (!event.currentTarget.contains(event.target)) { return; } if (isNavigationKey(event.key) && event.shiftKey) { // The cell that has focus after the keyboard navigation var focusCell = gridFocusCellSelector(apiRef); if (focusCell && focusCell.id !== params.id) { event.preventDefault(); var isNextRowSelected = apiRef.current.isRowSelected(focusCell.id); if (!canHaveMultipleSelection) { apiRef.current.selectRow(focusCell.id, !isNextRowSelected, true); return; } var newRowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(focusCell.id); var previousRowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(params.id); var start; var end; if (newRowIndex > previousRowIndex) { if (isNextRowSelected) { // We are navigating to the bottom of the page and adding selected rows start = previousRowIndex; end = newRowIndex - 1; } else { // We are navigating to the bottom of the page and removing selected rows start = previousRowIndex; end = newRowIndex; } } else { // eslint-disable-next-line no-lonely-if if (isNextRowSelected) { // We are navigating to the top of the page and removing selected rows start = newRowIndex + 1; end = previousRowIndex; } else { // We are navigating to the top of the page and adding selected rows start = newRowIndex; end = previousRowIndex; } } var rowsBetweenStartAndEnd = visibleRows.rows.slice(start, end + 1).map(function (row) { return row.id; }); apiRef.current.selectRows(rowsBetweenStartAndEnd, !isNextRowSelected); return; } } if (event.key === ' ' && event.shiftKey) { event.preventDefault(); handleSingleRowSelection(params.id, event); return; } if (event.key.toLowerCase() === 'a' && (event.ctrlKey || event.metaKey)) { event.preventDefault(); selectRows(apiRef.current.getAllRowIds(), true); } }, [apiRef, handleSingleRowSelection, selectRows, visibleRows.rows, canHaveMultipleSelection]); useGridApiEventHandler(apiRef, 'sortedRowsSet', removeOutdatedSelection); useGridApiEventHandler(apiRef, 'cellClick', handleCellClick); useGridApiEventHandler(apiRef, 'rowSelectionCheckboxChange', handleRowSelectionCheckboxChange); useGridApiEventHandler(apiRef, 'headerSelectionCheckboxChange', handleHeaderSelectionCheckboxChange); useGridApiEventHandler(apiRef, 'cellMouseDown', preventSelectionOnShift); useGridApiEventHandler(apiRef, 'cellKeyDown', handleCellKeyDown); /** * EFFECTS */ React.useEffect(function () { if (propSelectionModel !== undefined) { apiRef.current.setSelectionModel(propSelectionModel); } }, [apiRef, propSelectionModel]); var isStateControlled = propSelectionModel != null; React.useEffect(function () { if (isStateControlled) { return; } // props.isRowSelectable changed var currentSelection = gridSelectionStateSelector(apiRef.current.state); if (isRowSelectable) { var newSelection = currentSelection.filter(function (id) { return isRowSelectable(id); }); if (newSelection.length < currentSelection.length) { apiRef.current.setSelectionModel(newSelection); } } }, [apiRef, isRowSelectable, isStateControlled]); React.useEffect(function () { var currentSelection = gridSelectionStateSelector(apiRef.current.state); if (!canHaveMultipleSelection && currentSelection.length > 1) { var _getVisibleRows = getVisibleRows(apiRef, { pagination: pagination, paginationMode: paginationMode }), currentPageRows = _getVisibleRows.rows; var currentPageRowsLookup = currentPageRows.reduce(function (acc, _ref4) { var id = _ref4.id; acc[id] = true; return acc; }, {}); var firstSelectableRow = currentSelection.find(function (id) { var isSelectable = true; if (isRowSelectable) { isSelectable = isRowSelectable(id); } return isSelectable && currentPageRowsLookup[id]; // Check if the row is in the current page }); apiRef.current.setSelectionModel(firstSelectableRow !== undefined ? [firstSelectableRow] : []); } }, [apiRef, canHaveMultipleSelection, checkboxSelection, disableMultipleSelection, isRowSelectable, pagination, paginationMode]); };