UNPKG

@mui/x-data-grid-premium

Version:

The Premium plan edition of the MUI X Data Grid Components.

492 lines (489 loc) 19.2 kB
'use client'; import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import ownerDocument from '@mui/utils/ownerDocument'; import useEventCallback from '@mui/utils/useEventCallback'; import { getTotalHeaderHeight, getVisibleRows, isNavigationKey, serializeCellValue, useGridRegisterPipeProcessor } from '@mui/x-data-grid-pro/internals'; 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'; import { gridCellSelectionStateSelector } from "./gridCellSelectionSelector.js"; export const cellSelectionStateInitializer = (state, props) => _extends({}, state, { cellSelection: _extends({}, props.cellSelectionModel ?? props.initialState?.cellSelection) }); function isKeyboardEvent(event) { return !!event.key; } const AUTO_SCROLL_SENSITIVITY = 50; // The distance from the edge to start scrolling const AUTO_SCROLL_SPEED = 20; // The speed to scroll once the mouse enters the sensitivity area export const useGridCellSelection = (apiRef, props) => { const hasRootReference = apiRef.current.rootElementRef.current !== null; const cellWithVirtualFocus = React.useRef(null); const lastMouseDownCell = React.useRef(null); const mousePosition = React.useRef(null); const autoScrollRAF = React.useRef(null); const totalHeaderHeight = getTotalHeaderHeight(apiRef, props); const ignoreValueFormatterProp = props.ignoreValueFormatterDuringExport; const ignoreValueFormatter = (typeof ignoreValueFormatterProp === 'object' ? ignoreValueFormatterProp?.clipboardExport : ignoreValueFormatterProp) || false; const clipboardCopyCellDelimiter = props.clipboardCopyCellDelimiter; apiRef.current.registerControlState({ stateId: 'cellSelection', propModel: props.cellSelectionModel, propOnChange: props.onCellSelectionModelChange, stateSelector: gridCellSelectionStateSelector, changeEvent: 'cellSelectionChange' }); const runIfCellSelectionIsEnabled = callback => (...args) => { if (props.cellSelection) { callback(...args); } }; const isCellSelected = React.useCallback((id, field) => { if (!props.cellSelection) { return false; } const cellSelectionModel = gridCellSelectionStateSelector(apiRef); return cellSelectionModel[id] ? !!cellSelectionModel[id][field] : false; }, [apiRef, props.cellSelection]); const getCellSelectionModel = React.useCallback(() => { return gridCellSelectionStateSelector(apiRef); }, [apiRef]); const setCellSelectionModel = React.useCallback(newModel => { if (!props.cellSelection) { return; } apiRef.current.setState(prevState => _extends({}, prevState, { cellSelection: newModel })); }, [apiRef, props.cellSelection]); const selectCellRange = React.useCallback((start, end, keepOtherSelected = false) => { const startRowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(start.id); const startColumnIndex = apiRef.current.getColumnIndex(start.field); const endRowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(end.id); const endColumnIndex = apiRef.current.getColumnIndex(end.field); let finalStartRowIndex = startRowIndex; let finalStartColumnIndex = startColumnIndex; let finalEndRowIndex = endRowIndex; let finalEndColumnIndex = endColumnIndex; if (finalStartRowIndex > finalEndRowIndex) { finalStartRowIndex = endRowIndex; finalEndRowIndex = startRowIndex; } if (finalStartColumnIndex > finalEndColumnIndex) { finalStartColumnIndex = endColumnIndex; finalEndColumnIndex = startColumnIndex; } const visibleColumns = apiRef.current.getVisibleColumns(); const visibleRows = getVisibleRows(apiRef); const rowsInRange = visibleRows.rows.slice(finalStartRowIndex, finalEndRowIndex + 1); const columnsInRange = visibleColumns.slice(finalStartColumnIndex, finalEndColumnIndex + 1); const newModel = keepOtherSelected ? _extends({}, apiRef.current.getCellSelectionModel()) : {}; rowsInRange.forEach(row => { if (!newModel[row.id]) { newModel[row.id] = {}; } columnsInRange.forEach(column => { newModel[row.id][column.field] = true; }, {}); }); apiRef.current.setCellSelectionModel(newModel); }, [apiRef]); const getSelectedCellsAsArray = React.useCallback(() => { const selectionModel = apiRef.current.getCellSelectionModel(); const currentVisibleRows = getVisibleRows(apiRef, props); const sortedEntries = currentVisibleRows.rows.reduce((result, row) => { if (row.id in selectionModel) { result.push([row.id, selectionModel[row.id]]); } return result; }, []); return sortedEntries.reduce((selectedCells, [id, fields]) => { selectedCells.push(...Object.entries(fields).reduce((selectedFields, [field, isSelected]) => { if (isSelected) { selectedFields.push({ id, field }); } return selectedFields; }, [])); return selectedCells; }, []); }, [apiRef, props]); const cellSelectionApi = { isCellSelected, getCellSelectionModel, setCellSelectionModel, selectCellRange, getSelectedCellsAsArray }; useGridApiMethod(apiRef, cellSelectionApi, 'public'); const hasClickedValidCellForRangeSelection = React.useCallback(params => { if (params.field === GRID_CHECKBOX_SELECTION_COL_DEF.field) { return false; } if (params.field === GRID_DETAIL_PANEL_TOGGLE_FIELD) { return false; } const column = apiRef.current.getColumn(params.field); if (column.type === GRID_ACTIONS_COLUMN_TYPE) { return false; } return params.rowNode.type !== 'pinnedRow'; }, [apiRef]); const handleMouseUp = useEventCallback(() => { lastMouseDownCell.current = null; apiRef.current.rootElementRef?.current?.classList.remove(gridClasses['root--disableUserSelection']); // eslint-disable-next-line @typescript-eslint/no-use-before-define stopAutoScroll(); }); const handleCellMouseDown = React.useCallback((params, event) => { // Skip if the click comes from the right-button or, only on macOS, Ctrl is pressed // Fix for https://github.com/mui/mui-x/pull/6567#issuecomment-1329155578 const isMacOs = window.navigator.platform.toUpperCase().indexOf('MAC') >= 0; if (event.button !== 0 || event.ctrlKey && isMacOs) { return; } if (params.field === GRID_REORDER_COL_DEF.field) { return; } const focusedCell = gridFocusCellSelector(apiRef); if (hasClickedValidCellForRangeSelection(params) && event.shiftKey && focusedCell) { event.preventDefault(); } lastMouseDownCell.current = { id: params.id, field: params.field }; apiRef.current.rootElementRef?.current?.classList.add(gridClasses['root--disableUserSelection']); const document = ownerDocument(apiRef.current.rootElementRef?.current); document.addEventListener('mouseup', handleMouseUp, { once: true }); }, [apiRef, handleMouseUp, hasClickedValidCellForRangeSelection]); const stopAutoScroll = React.useCallback(() => { if (autoScrollRAF.current) { cancelAnimationFrame(autoScrollRAF.current); autoScrollRAF.current = null; } }, []); const handleCellFocusIn = React.useCallback(params => { cellWithVirtualFocus.current = { id: params.id, field: params.field }; }, []); const startAutoScroll = React.useCallback(() => { if (autoScrollRAF.current) { return; } if (!apiRef.current.virtualScrollerRef?.current) { return; } function autoScroll() { if (!mousePosition.current || !apiRef.current.virtualScrollerRef?.current) { return; } const dimensions = gridDimensionsSelector(apiRef); const { x: mouseX, y: mouseY } = mousePosition.current; const { width, height: viewportOuterHeight } = dimensions.viewportOuterSize; const height = viewportOuterHeight - totalHeaderHeight; let deltaX = 0; let deltaY = 0; let factor = 0; if (mouseY <= AUTO_SCROLL_SENSITIVITY && dimensions.hasScrollY) { // When scrolling up, the multiplier increases going closer to the top edge factor = (AUTO_SCROLL_SENSITIVITY - mouseY) / -AUTO_SCROLL_SENSITIVITY; deltaY = AUTO_SCROLL_SPEED; } else if (mouseY >= height - AUTO_SCROLL_SENSITIVITY && dimensions.hasScrollY) { // When scrolling down, the multiplier increases going closer to the bottom edge factor = (mouseY - (height - AUTO_SCROLL_SENSITIVITY)) / AUTO_SCROLL_SENSITIVITY; deltaY = AUTO_SCROLL_SPEED; } else if (mouseX <= AUTO_SCROLL_SENSITIVITY && dimensions.hasScrollX) { // When scrolling left, the multiplier increases going closer to the left edge factor = (AUTO_SCROLL_SENSITIVITY - mouseX) / -AUTO_SCROLL_SENSITIVITY; deltaX = AUTO_SCROLL_SPEED; } else if (mouseX >= width - AUTO_SCROLL_SENSITIVITY && dimensions.hasScrollX) { // When scrolling right, the multiplier increases going closer to the right edge factor = (mouseX - (width - AUTO_SCROLL_SENSITIVITY)) / AUTO_SCROLL_SENSITIVITY; deltaX = AUTO_SCROLL_SPEED; } if (deltaX !== 0 || deltaY !== 0) { const { scrollLeft, scrollTop } = apiRef.current.virtualScrollerRef.current; apiRef.current.scroll({ top: scrollTop + deltaY * factor, left: scrollLeft + deltaX * factor }); } autoScrollRAF.current = requestAnimationFrame(autoScroll); } autoScroll(); }, [apiRef, totalHeaderHeight]); const handleCellMouseOver = React.useCallback((params, event) => { if (!lastMouseDownCell.current) { return; } const { id, field } = params; apiRef.current.selectCellRange(lastMouseDownCell.current, { id, field }, event.ctrlKey || event.metaKey); const virtualScrollerRect = apiRef.current.virtualScrollerRef?.current?.getBoundingClientRect(); if (!virtualScrollerRect) { return; } const dimensions = gridDimensionsSelector(apiRef); const { x, y } = virtualScrollerRect; const { width, height: viewportOuterHeight } = dimensions.viewportOuterSize; const height = viewportOuterHeight - totalHeaderHeight; const mouseX = event.clientX - x; const mouseY = event.clientY - y - totalHeaderHeight; mousePosition.current = { x: mouseX, y: mouseY }; const hasEnteredVerticalSensitivityArea = mouseY <= AUTO_SCROLL_SENSITIVITY || mouseY >= height - AUTO_SCROLL_SENSITIVITY; const hasEnteredHorizontalSensitivityArea = mouseX <= AUTO_SCROLL_SENSITIVITY || mouseX >= width - AUTO_SCROLL_SENSITIVITY; const hasEnteredSensitivityArea = hasEnteredVerticalSensitivityArea || hasEnteredHorizontalSensitivityArea; if (hasEnteredSensitivityArea) { // Mouse has entered the sensitity area for the first time startAutoScroll(); } else { // Mouse has left the sensitivity area while auto scroll is on stopAutoScroll(); } }, [apiRef, startAutoScroll, stopAutoScroll, totalHeaderHeight]); const handleCellClick = useEventCallback((params, event) => { const { id, field } = params; if (!hasClickedValidCellForRangeSelection(params)) { return; } const focusedCell = gridFocusCellSelector(apiRef); if (event.shiftKey && focusedCell) { apiRef.current.selectCellRange(focusedCell, { id, field }); cellWithVirtualFocus.current = { id, field }; return; } if (event.ctrlKey || event.metaKey) { // Add the clicked cell to the selection const prevModel = apiRef.current.getCellSelectionModel(); apiRef.current.setCellSelectionModel(_extends({}, prevModel, { [id]: _extends({}, prevModel[id], { [field]: !apiRef.current.isCellSelected(id, field) }) })); } else { // Clear the selection and keep only the clicked cell selected apiRef.current.setCellSelectionModel({ [id]: { [field]: true } }); } }); const handleCellKeyDown = useEventCallback((params, event) => { if (!isNavigationKey(event.key) || !cellWithVirtualFocus.current) { return; } if (event.key === ' ' && params.cellMode === GridCellModes.Edit) { return; } if (!event.shiftKey) { apiRef.current.setCellSelectionModel({}); return; } const { current: otherCell } = cellWithVirtualFocus; let endRowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(otherCell.id); let endColumnIndex = apiRef.current.getColumnIndex(otherCell.field); if (event.key === 'ArrowDown') { endRowIndex += 1; } else if (event.key === 'ArrowUp') { endRowIndex -= 1; } else if (event.key === 'ArrowRight') { endColumnIndex += 1; } else if (event.key === 'ArrowLeft') { endColumnIndex -= 1; } const visibleRows = getVisibleRows(apiRef); if (endRowIndex < 0 || endRowIndex >= visibleRows.rows.length) { return; } const visibleColumns = apiRef.current.getVisibleColumns(); if (endColumnIndex < 0 || endColumnIndex >= visibleColumns.length) { return; } cellWithVirtualFocus.current = { id: visibleRows.rows[endRowIndex].id, field: visibleColumns[endColumnIndex].field }; apiRef.current.scrollToIndexes({ rowIndex: endRowIndex, colIndex: endColumnIndex }); const { id, field } = params; apiRef.current.selectCellRange({ id, field }, cellWithVirtualFocus.current); }); useGridEvent(apiRef, 'cellClick', runIfCellSelectionIsEnabled(handleCellClick)); useGridEvent(apiRef, 'cellFocusIn', runIfCellSelectionIsEnabled(handleCellFocusIn)); useGridEvent(apiRef, 'cellKeyDown', runIfCellSelectionIsEnabled(handleCellKeyDown)); useGridEvent(apiRef, 'cellMouseDown', runIfCellSelectionIsEnabled(handleCellMouseDown)); useGridEvent(apiRef, 'cellMouseOver', runIfCellSelectionIsEnabled(handleCellMouseOver)); React.useEffect(() => { if (props.cellSelectionModel) { apiRef.current.setCellSelectionModel(props.cellSelectionModel); } }, [apiRef, props.cellSelectionModel]); React.useEffect(() => { const rootRef = apiRef.current.rootElementRef?.current; return () => { stopAutoScroll(); const document = ownerDocument(rootRef); document.removeEventListener('mouseup', handleMouseUp); }; }, [apiRef, hasRootReference, handleMouseUp, stopAutoScroll]); const checkIfCellIsSelected = React.useCallback((isSelected, { id, field }) => { return apiRef.current.isCellSelected(id, field); }, [apiRef]); const addClassesToCells = React.useCallback((classes, { id, field }) => { const visibleRows = getVisibleRows(apiRef); if (!visibleRows.range || !apiRef.current.isCellSelected(id, field)) { return classes; } const newClasses = [...classes]; const rowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(id); const columnIndex = apiRef.current.getColumnIndex(field); const visibleColumns = apiRef.current.getVisibleColumns(); if (rowIndex > 0) { const { id: previousRowId } = visibleRows.rows[rowIndex - 1]; if (!apiRef.current.isCellSelected(previousRowId, field)) { newClasses.push(gridClasses['cell--rangeTop']); } } else { newClasses.push(gridClasses['cell--rangeTop']); } if (rowIndex + visibleRows.range.firstRowIndex < visibleRows.range.lastRowIndex) { const { id: nextRowId } = visibleRows.rows[rowIndex + 1]; if (!apiRef.current.isCellSelected(nextRowId, field)) { newClasses.push(gridClasses['cell--rangeBottom']); } } else { newClasses.push(gridClasses['cell--rangeBottom']); } if (columnIndex > 0) { const { field: previousColumnField } = visibleColumns[columnIndex - 1]; if (!apiRef.current.isCellSelected(id, previousColumnField)) { newClasses.push(gridClasses['cell--rangeLeft']); } } else { newClasses.push(gridClasses['cell--rangeLeft']); } if (columnIndex < visibleColumns.length - 1) { const { field: nextColumnField } = visibleColumns[columnIndex + 1]; if (!apiRef.current.isCellSelected(id, nextColumnField)) { newClasses.push(gridClasses['cell--rangeRight']); } } else { newClasses.push(gridClasses['cell--rangeRight']); } return newClasses; }, [apiRef]); const canUpdateFocus = React.useCallback((initialValue, { event, cell }) => { if (!cell || !props.cellSelection || !event.shiftKey) { return initialValue; } if (isKeyboardEvent(event)) { return isNavigationKey(event.key) ? false : initialValue; } const focusedCell = gridFocusCellSelector(apiRef); if (hasClickedValidCellForRangeSelection(cell) && focusedCell) { return false; } return initialValue; }, [apiRef, props.cellSelection, hasClickedValidCellForRangeSelection]); const handleClipboardCopy = React.useCallback(value => { if (apiRef.current.getSelectedCellsAsArray().length <= 1) { return value; } const sortedRowIds = gridSortedRowIdsSelector(apiRef); const cellSelectionModel = apiRef.current.getCellSelectionModel(); const unsortedSelectedRowIds = Object.keys(cellSelectionModel); const sortedSelectedRowIds = sortedRowIds.filter(id => unsortedSelectedRowIds.includes(`${id}`)); const copyData = sortedSelectedRowIds.reduce((acc, rowId) => { const fieldsMap = cellSelectionModel[rowId]; const rowValues = Object.keys(fieldsMap).map(field => { let cellData; if (fieldsMap[field]) { const cellParams = apiRef.current.getCellParams(rowId, field); cellData = serializeCellValue(cellParams, { csvOptions: { delimiter: clipboardCopyCellDelimiter, shouldAppendQuotes: false, escapeFormulas: false }, ignoreValueFormatter }); } else { cellData = ''; } return cellData; }, ''); const rowString = rowValues.join(clipboardCopyCellDelimiter); return acc === '' ? rowString : [acc, rowString].join('\r\n'); }, ''); return copyData; }, [apiRef, ignoreValueFormatter, clipboardCopyCellDelimiter]); useGridRegisterPipeProcessor(apiRef, 'isCellSelected', checkIfCellIsSelected); useGridRegisterPipeProcessor(apiRef, 'cellClassName', addClassesToCells); useGridRegisterPipeProcessor(apiRef, 'canUpdateFocus', canUpdateFocus); useGridRegisterPipeProcessor(apiRef, 'clipboardCopy', handleClipboardCopy); };