UNPKG

@mui/x-data-grid-premium

Version:

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

321 lines (318 loc) 11.3 kB
import _extends from "@babel/runtime/helpers/esm/extends"; import * as React from 'react'; import { GRID_CHECKBOX_SELECTION_FIELD, gridFocusCellSelector, gridVisibleColumnFieldsSelector, useGridApiOptionHandler, useGridApiEventHandler, gridPaginatedVisibleSortedGridRowIdsSelector, gridExpandedSortedRowIdsSelector } from '@mui/x-data-grid'; import { buildWarning, getRowIdFromRowModel, getActiveElement, useGridRegisterPipeProcessor, getPublicApiRef, isPasteShortcut, useGridLogger } from '@mui/x-data-grid/internals'; import { GRID_DETAIL_PANEL_TOGGLE_FIELD, GRID_REORDER_COL_DEF } from '@mui/x-data-grid-pro'; import { unstable_debounce as debounce } from '@mui/utils'; const missingOnProcessRowUpdateErrorWarning = buildWarning(['MUI X: A call to `processRowUpdate` threw an error which was not handled because `onProcessRowUpdateError` is missing.', 'To handle the error pass a callback to the `onProcessRowUpdateError` prop, for example `<DataGrid onProcessRowUpdateError={(error) => ...} />`.', 'For more detail, see https://mui.com/x/react-data-grid/editing/#server-side-persistence.'], 'error'); const columnFieldsToExcludeFromPaste = [GRID_CHECKBOX_SELECTION_FIELD, GRID_REORDER_COL_DEF.field, GRID_DETAIL_PANEL_TOGGLE_FIELD]; // Batches rows that are updated during clipboard paste to reduce `updateRows` calls function batchRowUpdates(func, wait) { let rows = []; const debounced = debounce(() => { func(rows); rows = []; }, wait); return row => { rows.push(row); debounced(); }; } async function getTextFromClipboard(rootEl) { return new Promise(resolve => { const focusedCell = getActiveElement(document); const el = document.createElement('input'); el.style.width = '0px'; el.style.height = '0px'; el.style.border = 'none'; el.style.margin = '0'; el.style.padding = '0'; el.style.outline = 'none'; el.style.position = 'absolute'; el.style.top = '0'; el.style.left = '0'; const handlePasteEvent = event => { el.removeEventListener('paste', handlePasteEvent); const text = event.clipboardData?.getData('text/plain'); if (focusedCell instanceof HTMLElement) { focusedCell.focus({ preventScroll: true }); } el.remove(); resolve(text || ''); }; el.addEventListener('paste', handlePasteEvent); rootEl.appendChild(el); el.focus({ preventScroll: true }); }); } // Keeps track of updated rows during clipboard paste class CellValueUpdater { constructor(options) { this.rowsToUpdate = {}; this.updateRow = void 0; this.options = void 0; this.options = options; this.updateRow = batchRowUpdates(options.apiRef.current.updateRows, 50); } updateCell({ rowId, field, pastedCellValue }) { if (pastedCellValue === undefined) { return; } const { apiRef, getRowId } = this.options; const colDef = apiRef.current.getColumn(field); if (!colDef || !colDef.editable) { return; } const row = this.rowsToUpdate[rowId] || _extends({}, apiRef.current.getRow(rowId)); if (!row) { return; } let parsedValue = pastedCellValue; if (colDef.pastedValueParser) { parsedValue = colDef.pastedValueParser(pastedCellValue, row, colDef, apiRef); } else if (colDef.valueParser) { parsedValue = colDef.valueParser(parsedValue, row, colDef, apiRef); } if (parsedValue === undefined) { return; } let rowCopy = _extends({}, row); if (typeof colDef.valueSetter === 'function') { rowCopy = colDef.valueSetter(parsedValue, rowCopy, colDef, apiRef); } else { rowCopy[field] = parsedValue; } const newRowId = getRowIdFromRowModel(rowCopy, getRowId); if (String(newRowId) !== String(rowId)) { // We cannot update row id, so this cell value update should be ignored return; } this.rowsToUpdate[rowId] = rowCopy; } applyUpdates() { const { apiRef, processRowUpdate, onProcessRowUpdateError } = this.options; const rowsToUpdate = this.rowsToUpdate; const rowIdsToUpdate = Object.keys(rowsToUpdate); if (rowIdsToUpdate.length === 0) { apiRef.current.publishEvent('clipboardPasteEnd'); return; } const handleRowUpdate = async rowId => { const newRow = rowsToUpdate[rowId]; if (typeof processRowUpdate === 'function') { const handleError = errorThrown => { if (onProcessRowUpdateError) { onProcessRowUpdateError(errorThrown); } else if (process.env.NODE_ENV !== 'production') { missingOnProcessRowUpdateErrorWarning(); } }; try { const oldRow = apiRef.current.getRow(rowId); const finalRowUpdate = await processRowUpdate(newRow, oldRow); this.updateRow(finalRowUpdate); } catch (error) { handleError(error); } } else { this.updateRow(newRow); } }; const promises = rowIdsToUpdate.map(rowId => { // Wrap in promise that always resolves to avoid Promise.all from stopping on first error. // This is to avoid using `Promise.allSettled` that has worse browser support. return new Promise(resolve => { handleRowUpdate(rowId).then(resolve).catch(resolve); }); }); Promise.all(promises).then(() => { this.rowsToUpdate = {}; apiRef.current.publishEvent('clipboardPasteEnd'); }); } } function defaultPasteResolver({ pastedData, apiRef, updateCell, pagination }) { const isSingleValuePasted = pastedData.length === 1 && pastedData[0].length === 1; const cellSelectionModel = apiRef.current.getCellSelectionModel(); const selectedCellsArray = apiRef.current.getSelectedCellsAsArray(); if (cellSelectionModel && selectedCellsArray.length > 1) { Object.keys(cellSelectionModel).forEach((rowId, rowIndex) => { const rowDataArr = pastedData[isSingleValuePasted ? 0 : rowIndex]; const hasRowData = isSingleValuePasted ? true : rowDataArr !== undefined; if (!hasRowData) { return; } Object.keys(cellSelectionModel[rowId]).forEach((field, colIndex) => { const cellValue = isSingleValuePasted ? rowDataArr[0] : rowDataArr[colIndex]; updateCell({ rowId, field, pastedCellValue: cellValue }); }); }); return; } const visibleColumnFields = gridVisibleColumnFieldsSelector(apiRef).filter(field => { if (columnFieldsToExcludeFromPaste.includes(field)) { return false; } return true; }); const selectedRows = apiRef.current.getSelectedRows(); if (selectedRows.size > 0 && !isSingleValuePasted) { // Multiple values are pasted starting from the first and top-most cell const pastedRowsDataCount = pastedData.length; // There's no guarantee that the selected rows are in the same order as the pasted rows selectedRows.forEach((row, rowId) => { let rowData; if (pastedRowsDataCount === 1) { // If only one row is pasted - paste it to all selected rows rowData = pastedData[0]; } else { rowData = pastedData.shift(); } if (rowData === undefined) { return; } rowData.forEach((newCellValue, cellIndex) => { updateCell({ rowId, field: visibleColumnFields[cellIndex], pastedCellValue: newCellValue }); }); }); return; } let selectedCell = gridFocusCellSelector(apiRef); if (!selectedCell && selectedCellsArray.length === 1) { selectedCell = selectedCellsArray[0]; } if (!selectedCell) { return; } if (columnFieldsToExcludeFromPaste.includes(selectedCell.field)) { return; } const selectedRowId = selectedCell.id; const selectedRowIndex = apiRef.current.getRowIndexRelativeToVisibleRows(selectedRowId); const visibleRowIds = pagination ? gridPaginatedVisibleSortedGridRowIdsSelector(apiRef) : gridExpandedSortedRowIdsSelector(apiRef); const selectedFieldIndex = visibleColumnFields.indexOf(selectedCell.field); pastedData.forEach((rowData, index) => { const rowId = visibleRowIds[selectedRowIndex + index]; if (typeof rowId === 'undefined') { return; } for (let i = selectedFieldIndex; i < visibleColumnFields.length; i += 1) { const field = visibleColumnFields[i]; const stringValue = rowData[i - selectedFieldIndex]; updateCell({ rowId, field, pastedCellValue: stringValue }); } }); } export const useGridClipboardImport = (apiRef, props) => { const processRowUpdate = props.processRowUpdate; const onProcessRowUpdateError = props.onProcessRowUpdateError; const getRowId = props.getRowId; const enableClipboardPaste = !props.disableClipboardPaste; const rootEl = apiRef.current.rootElementRef?.current; const logger = useGridLogger(apiRef, 'useGridClipboardImport'); const splitClipboardPastedText = props.splitClipboardPastedText; const { pagination, onBeforeClipboardPasteStart } = props; const handlePaste = React.useCallback(async (params, event) => { if (!enableClipboardPaste) { return; } if (!isPasteShortcut(event)) { return; } const focusedCell = gridFocusCellSelector(apiRef); if (focusedCell !== null) { const cellMode = apiRef.current.getCellMode(focusedCell.id, focusedCell.field); if (cellMode === 'edit') { // Do not paste data when the cell is in edit mode return; } } if (!rootEl) { return; } const text = await getTextFromClipboard(rootEl); if (!text) { return; } const pastedData = splitClipboardPastedText(text); if (!pastedData) { return; } if (onBeforeClipboardPasteStart) { try { await onBeforeClipboardPasteStart({ data: pastedData }); } catch (error) { logger.debug('Clipboard paste operation cancelled'); return; } } const cellUpdater = new CellValueUpdater({ apiRef, processRowUpdate, onProcessRowUpdateError, getRowId }); apiRef.current.publishEvent('clipboardPasteStart', { data: pastedData }); defaultPasteResolver({ pastedData, apiRef: getPublicApiRef(apiRef), updateCell: (...args) => { cellUpdater.updateCell(...args); }, pagination }); cellUpdater.applyUpdates(); }, [apiRef, processRowUpdate, onProcessRowUpdateError, getRowId, enableClipboardPaste, rootEl, splitClipboardPastedText, pagination, onBeforeClipboardPasteStart, logger]); const checkIfCanStartEditing = React.useCallback((initialValue, { event }) => { if (isPasteShortcut(event) && enableClipboardPaste) { // Do not enter cell edit mode on paste return false; } return initialValue; }, [enableClipboardPaste]); useGridApiEventHandler(apiRef, 'cellKeyDown', handlePaste); useGridApiOptionHandler(apiRef, 'clipboardPasteStart', props.onClipboardPasteStart); useGridApiOptionHandler(apiRef, 'clipboardPasteEnd', props.onClipboardPasteEnd); useGridRegisterPipeProcessor(apiRef, 'canStartEditing', checkIfCanStartEditing); };