UNPKG

@mui/x-data-grid

Version:

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

461 lines (456 loc) 21.5 kB
import _asyncToGenerator from "@babel/runtime/helpers/esm/asyncToGenerator"; import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties"; import _toPropertyKey from "@babel/runtime/helpers/esm/toPropertyKey"; import _defineProperty from "@babel/runtime/helpers/esm/defineProperty"; import _extends from "@babel/runtime/helpers/esm/extends"; import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray"; var _excluded = ["id", "field"], _excluded2 = ["id", "field"]; import _regeneratorRuntime from "@babel/runtime/regenerator"; import * as React from 'react'; import { unstable_useEventCallback as useEventCallback, unstable_useEnhancedEffect as useEnhancedEffect } from '@mui/utils'; import { useGridApiEventHandler, useGridApiOptionHandler } from '../../utils/useGridApiEventHandler'; import { GridEditModes, GridCellModes } from '../../../models/gridEditRowModel'; import { useGridApiMethod } from '../../utils/useGridApiMethod'; import { gridEditRowsStateSelector } from './gridEditingSelectors'; import { isPrintableKey } from '../../../utils/keyboardUtils'; import { buildWarning } from '../../../utils/warning'; import { gridRowsDataRowIdToIdLookupSelector } from '../rows/gridRowsSelector'; import { deepClone } from '../../../utils/utils'; import { GridCellEditStartReasons, GridCellEditStopReasons } from '../../../models/params/gridEditCellParams'; var missingOnProcessRowUpdateErrorWarning = buildWarning(['MUI: 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, e.g. `<DataGrid onProcessRowUpdateError={(error) => ...} />`.', 'For more detail, see http://mui.com/components/data-grid/editing/#server-side-persistence.'], 'error'); export var useGridCellEditing = function useGridCellEditing(apiRef, props) { var _React$useState = React.useState({}), _React$useState2 = _slicedToArray(_React$useState, 2), cellModesModel = _React$useState2[0], setCellModesModel = _React$useState2[1]; var cellModesModelRef = React.useRef(cellModesModel); var prevCellModesModel = React.useRef({}); var processRowUpdate = props.processRowUpdate, onProcessRowUpdateError = props.onProcessRowUpdateError, cellModesModelProp = props.cellModesModel, onCellModesModelChange = props.onCellModesModelChange; var runIfEditModeIsCell = function runIfEditModeIsCell(callback) { return function () { if (props.editMode === GridEditModes.Cell) { callback.apply(void 0, arguments); } }; }; var throwIfNotEditable = React.useCallback(function (id, field) { var params = apiRef.current.getCellParams(id, field); if (!apiRef.current.isCellEditable(params)) { throw new Error("MUI: The cell with id=".concat(id, " and field=").concat(field, " is not editable.")); } }, [apiRef]); var throwIfNotInMode = React.useCallback(function (id, field, mode) { if (apiRef.current.getCellMode(id, field) !== mode) { throw new Error("MUI: The cell with id=".concat(id, " and field=").concat(field, " is not in ").concat(mode, " mode.")); } }, [apiRef]); var handleCellDoubleClick = React.useCallback(function (params, event) { if (!params.isEditable) { return; } if (params.cellMode === GridCellModes.Edit) { return; } var newParams = _extends({}, params, { reason: GridCellEditStartReasons.cellDoubleClick }); apiRef.current.publishEvent('cellEditStart', newParams, event); }, [apiRef]); var handleCellFocusOut = React.useCallback(function (params, event) { if (params.cellMode === GridCellModes.View) { return; } if (apiRef.current.getCellMode(params.id, params.field) === GridCellModes.View) { return; } var newParams = _extends({}, params, { reason: GridCellEditStopReasons.cellFocusOut }); apiRef.current.publishEvent('cellEditStop', newParams, event); }, [apiRef]); var handleCellKeyDown = React.useCallback(function (params, event) { if (params.cellMode === GridCellModes.Edit) { // Wait until IME is settled for Asian languages like Japanese and Chinese // TODO: `event.which` is deprecated but this is a temporary workaround if (event.which === 229) { return; } var reason; if (event.key === 'Escape') { reason = GridCellEditStopReasons.escapeKeyDown; } else if (event.key === 'Enter') { reason = GridCellEditStopReasons.enterKeyDown; } else if (event.key === 'Tab') { reason = event.shiftKey ? GridCellEditStopReasons.shiftTabKeyDown : GridCellEditStopReasons.tabKeyDown; event.preventDefault(); // Prevent going to the next element in the tab sequence } if (reason) { var newParams = _extends({}, params, { reason: reason }); apiRef.current.publishEvent('cellEditStop', newParams, event); } } else if (params.isEditable) { var _reason; var canStartEditing = apiRef.current.unstable_applyPipeProcessors('canStartEditing', true, { event: event, cellParams: params, editMode: 'cell' }); if (!canStartEditing) { return; } if (isPrintableKey(event)) { _reason = GridCellEditStartReasons.printableKeyDown; } else if ((event.ctrlKey || event.metaKey) && event.key === 'v') { _reason = GridCellEditStartReasons.pasteKeyDown; } else if (event.key === 'Enter') { _reason = GridCellEditStartReasons.enterKeyDown; } else if (event.key === 'Delete' || event.key === 'Backspace') { // Delete on Windows, Backspace on macOS _reason = GridCellEditStartReasons.deleteKeyDown; } if (_reason) { var _newParams = _extends({}, params, { reason: _reason, key: event.key }); apiRef.current.publishEvent('cellEditStart', _newParams, event); } } }, [apiRef]); var handleCellEditStart = React.useCallback(function (params) { var id = params.id, field = params.field, reason = params.reason; var startCellEditModeParams = { id: id, field: field }; if (reason === GridCellEditStartReasons.printableKeyDown || reason === GridCellEditStartReasons.deleteKeyDown || reason === GridCellEditStartReasons.pasteKeyDown) { startCellEditModeParams.deleteValue = true; } apiRef.current.startCellEditMode(startCellEditModeParams); }, [apiRef]); var handleCellEditStop = React.useCallback(function (params) { var id = params.id, field = params.field, reason = params.reason; apiRef.current.runPendingEditCellValueMutation(id, field); var cellToFocusAfter; if (reason === GridCellEditStopReasons.enterKeyDown) { cellToFocusAfter = 'below'; } else if (reason === GridCellEditStopReasons.tabKeyDown) { cellToFocusAfter = 'right'; } else if (reason === GridCellEditStopReasons.shiftTabKeyDown) { cellToFocusAfter = 'left'; } var ignoreModifications = reason === 'escapeKeyDown'; apiRef.current.stopCellEditMode({ id: id, field: field, ignoreModifications: ignoreModifications, cellToFocusAfter: cellToFocusAfter }); }, [apiRef]); useGridApiEventHandler(apiRef, 'cellDoubleClick', runIfEditModeIsCell(handleCellDoubleClick)); useGridApiEventHandler(apiRef, 'cellFocusOut', runIfEditModeIsCell(handleCellFocusOut)); useGridApiEventHandler(apiRef, 'cellKeyDown', runIfEditModeIsCell(handleCellKeyDown)); useGridApiEventHandler(apiRef, 'cellEditStart', runIfEditModeIsCell(handleCellEditStart)); useGridApiEventHandler(apiRef, 'cellEditStop', runIfEditModeIsCell(handleCellEditStop)); useGridApiOptionHandler(apiRef, 'cellEditStart', props.onCellEditStart); useGridApiOptionHandler(apiRef, 'cellEditStop', props.onCellEditStop); var getCellMode = React.useCallback(function (id, field) { var editingState = gridEditRowsStateSelector(apiRef.current.state); var isEditing = editingState[id] && editingState[id][field]; return isEditing ? GridCellModes.Edit : GridCellModes.View; }, [apiRef]); var updateCellModesModel = useEventCallback(function (newModel) { var isNewModelDifferentFromProp = newModel !== props.cellModesModel; if (onCellModesModelChange && isNewModelDifferentFromProp) { onCellModesModelChange(newModel, {}); } if (props.cellModesModel && isNewModelDifferentFromProp) { return; // The prop always win } setCellModesModel(newModel); cellModesModelRef.current = newModel; apiRef.current.publishEvent('cellModesModelChange', newModel); }); var updateFieldInCellModesModel = React.useCallback(function (id, field, newProps) { // We use the ref because it always contain the up-to-date value, different from the state // that needs a rerender to reflect the new value var newModel = _extends({}, cellModesModelRef.current); if (newProps !== null) { newModel[id] = _extends({}, newModel[id], _defineProperty({}, field, _extends({}, newProps))); } else { var _newModel$id = newModel[id], fieldToRemove = _newModel$id[field], otherFields = _objectWithoutProperties(_newModel$id, [field].map(_toPropertyKey)); // Ensure that we have a new object, not a reference newModel[id] = otherFields; if (Object.keys(newModel[id]).length === 0) { delete newModel[id]; } } updateCellModesModel(newModel); }, [updateCellModesModel]); var updateOrDeleteFieldState = React.useCallback(function (id, field, newProps) { apiRef.current.setState(function (state) { var newEditingState = _extends({}, state.editRows); if (newProps !== null) { newEditingState[id] = _extends({}, newEditingState[id], _defineProperty({}, field, _extends({}, newProps))); } else { delete newEditingState[id][field]; if (Object.keys(newEditingState[id]).length === 0) { delete newEditingState[id]; } } return _extends({}, state, { editRows: newEditingState }); }); apiRef.current.forceUpdate(); }, [apiRef]); var startCellEditMode = React.useCallback(function (params) { var id = params.id, field = params.field, other = _objectWithoutProperties(params, _excluded); throwIfNotEditable(id, field); throwIfNotInMode(id, field, GridCellModes.View); updateFieldInCellModesModel(id, field, _extends({ mode: GridCellModes.Edit }, other)); }, [throwIfNotEditable, throwIfNotInMode, updateFieldInCellModesModel]); var updateStateToStartCellEditMode = useEventCallback(function (params) { var id = params.id, field = params.field, deleteValue = params.deleteValue, initialValue = params.initialValue; var newValue = apiRef.current.getCellValue(id, field); if (deleteValue || initialValue) { newValue = deleteValue ? '' : initialValue; } var newProps = { value: newValue, error: false, isProcessingProps: false }; updateOrDeleteFieldState(id, field, newProps); apiRef.current.setCellFocus(id, field); }); var stopCellEditMode = React.useCallback(function (params) { var id = params.id, field = params.field, other = _objectWithoutProperties(params, _excluded2); throwIfNotInMode(id, field, GridCellModes.Edit); updateFieldInCellModesModel(id, field, _extends({ mode: GridCellModes.View }, other)); }, [throwIfNotInMode, updateFieldInCellModesModel]); var updateStateToStopCellEditMode = useEventCallback( /*#__PURE__*/function () { var _ref = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(params) { var id, field, ignoreModifications, _params$cellToFocusAf, cellToFocusAfter, finishCellEditMode, editingState, _editingState$id$fiel, error, isProcessingProps, rowUpdate, handleError, row; return _regeneratorRuntime.wrap(function _callee$(_context) { while (1) switch (_context.prev = _context.next) { case 0: id = params.id, field = params.field, ignoreModifications = params.ignoreModifications, _params$cellToFocusAf = params.cellToFocusAfter, cellToFocusAfter = _params$cellToFocusAf === void 0 ? 'none' : _params$cellToFocusAf; throwIfNotInMode(id, field, GridCellModes.Edit); apiRef.current.runPendingEditCellValueMutation(id, field); finishCellEditMode = function finishCellEditMode() { updateOrDeleteFieldState(id, field, null); updateFieldInCellModesModel(id, field, null); if (cellToFocusAfter !== 'none') { apiRef.current.moveFocusToRelativeCell(id, field, cellToFocusAfter); } }; if (!ignoreModifications) { _context.next = 7; break; } finishCellEditMode(); return _context.abrupt("return"); case 7: editingState = gridEditRowsStateSelector(apiRef.current.state); _editingState$id$fiel = editingState[id][field], error = _editingState$id$fiel.error, isProcessingProps = _editingState$id$fiel.isProcessingProps; if (!(error || isProcessingProps)) { _context.next = 13; break; } // Attempt to change cell mode to "view" was not successful // Update previous mode to allow another attempt prevCellModesModel.current[id][field].mode = GridCellModes.Edit; // Revert the mode in the cellModesModel prop back to "edit" updateFieldInCellModesModel(id, field, { mode: GridCellModes.Edit }); return _context.abrupt("return"); case 13: rowUpdate = apiRef.current.getRowWithUpdatedValuesFromCellEditing(id, field); if (processRowUpdate) { handleError = function handleError(errorThrown) { prevCellModesModel.current[id][field].mode = GridCellModes.Edit; // Revert the mode in the cellModesModel prop back to "edit" updateFieldInCellModesModel(id, field, { mode: GridCellModes.Edit }); if (onProcessRowUpdateError) { onProcessRowUpdateError(errorThrown); } else { missingOnProcessRowUpdateErrorWarning(); } }; try { row = apiRef.current.getRow(id); Promise.resolve(processRowUpdate(rowUpdate, row)).then(function (finalRowUpdate) { apiRef.current.updateRows([finalRowUpdate]); finishCellEditMode(); }).catch(handleError); } catch (errorThrown) { handleError(errorThrown); } } else { apiRef.current.updateRows([rowUpdate]); finishCellEditMode(); } case 15: case "end": return _context.stop(); } }, _callee); })); return function (_x) { return _ref.apply(this, arguments); }; }()); var setCellEditingEditCellValue = React.useCallback( /*#__PURE__*/function () { var _ref2 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(params) { var _editingState$id; var id, field, value, debounceMs, skipValueParser, column, row, parsedValue, editingState, newProps, hasChanged; return _regeneratorRuntime.wrap(function _callee2$(_context2) { while (1) switch (_context2.prev = _context2.next) { case 0: id = params.id, field = params.field, value = params.value, debounceMs = params.debounceMs, skipValueParser = params.unstable_skipValueParser; throwIfNotEditable(id, field); throwIfNotInMode(id, field, GridCellModes.Edit); column = apiRef.current.getColumn(field); row = apiRef.current.getRow(id); parsedValue = value; if (column.valueParser && !skipValueParser) { parsedValue = column.valueParser(value, apiRef.current.getCellParams(id, field)); } editingState = gridEditRowsStateSelector(apiRef.current.state); newProps = _extends({}, editingState[id][field], { value: parsedValue, changeReason: debounceMs ? 'debouncedSetEditCellValue' : 'setEditCellValue' }); if (!column.preProcessEditCellProps) { _context2.next = 16; break; } hasChanged = value !== editingState[id][field].value; newProps = _extends({}, newProps, { isProcessingProps: true }); updateOrDeleteFieldState(id, field, newProps); _context2.next = 15; return Promise.resolve(column.preProcessEditCellProps({ id: id, row: row, props: newProps, hasChanged: hasChanged })); case 15: newProps = _context2.sent; case 16: if (!(apiRef.current.getCellMode(id, field) === GridCellModes.View)) { _context2.next = 18; break; } return _context2.abrupt("return", false); case 18: editingState = gridEditRowsStateSelector(apiRef.current.state); newProps = _extends({}, newProps, { isProcessingProps: false }); // We don't update the value with the one coming from the props pre-processing // because when the promise resolves it may be already outdated. The only // exception to this rule is when there's no pre-processing. newProps.value = column.preProcessEditCellProps ? editingState[id][field].value : parsedValue; updateOrDeleteFieldState(id, field, newProps); editingState = gridEditRowsStateSelector(apiRef.current.state); return _context2.abrupt("return", !((_editingState$id = editingState[id]) != null && (_editingState$id = _editingState$id[field]) != null && _editingState$id.error)); case 24: case "end": return _context2.stop(); } }, _callee2); })); return function (_x2) { return _ref2.apply(this, arguments); }; }(), [apiRef, throwIfNotEditable, throwIfNotInMode, updateOrDeleteFieldState]); var getRowWithUpdatedValuesFromCellEditing = React.useCallback(function (id, field) { var column = apiRef.current.getColumn(field); var editingState = gridEditRowsStateSelector(apiRef.current.state); var row = apiRef.current.getRow(id); if (!editingState[id] || !editingState[id][field]) { return apiRef.current.getRow(id); } var value = editingState[id][field].value; return column.valueSetter ? column.valueSetter({ value: value, row: row }) : _extends({}, row, _defineProperty({}, field, value)); }, [apiRef]); var editingApi = { getCellMode: getCellMode, startCellEditMode: startCellEditMode, stopCellEditMode: stopCellEditMode }; var editingPrivateApi = { setCellEditingEditCellValue: setCellEditingEditCellValue, getRowWithUpdatedValuesFromCellEditing: getRowWithUpdatedValuesFromCellEditing }; useGridApiMethod(apiRef, editingApi, 'public'); useGridApiMethod(apiRef, editingPrivateApi, 'private'); React.useEffect(function () { if (cellModesModelProp) { updateCellModesModel(cellModesModelProp); } }, [cellModesModelProp, updateCellModesModel]); // Run this effect synchronously so that the keyboard event can impact the yet-to-be-rendered input. useEnhancedEffect(function () { var idToIdLookup = gridRowsDataRowIdToIdLookupSelector(apiRef); // Update the ref here because updateStateToStopCellEditMode may change it later var copyOfPrevCellModes = prevCellModesModel.current; prevCellModesModel.current = deepClone(cellModesModel); // Do a deep-clone because the attributes might be changed later Object.entries(cellModesModel).forEach(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), id = _ref4[0], fields = _ref4[1]; Object.entries(fields).forEach(function (_ref5) { var _copyOfPrevCellModes$, _idToIdLookup$id; var _ref6 = _slicedToArray(_ref5, 2), field = _ref6[0], params = _ref6[1]; var prevMode = ((_copyOfPrevCellModes$ = copyOfPrevCellModes[id]) == null || (_copyOfPrevCellModes$ = _copyOfPrevCellModes$[field]) == null ? void 0 : _copyOfPrevCellModes$.mode) || GridCellModes.View; var originalId = (_idToIdLookup$id = idToIdLookup[id]) != null ? _idToIdLookup$id : id; if (params.mode === GridCellModes.Edit && prevMode === GridCellModes.View) { updateStateToStartCellEditMode(_extends({ id: originalId, field: field }, params)); } else if (params.mode === GridCellModes.View && prevMode === GridCellModes.Edit) { updateStateToStopCellEditMode(_extends({ id: originalId, field: field }, params)); } }); }); }, [apiRef, cellModesModel, updateStateToStartCellEditMode, updateStateToStopCellEditMode]); };