@mui/x-data-grid
Version:
The community edition of the data grid component (MUI X).
571 lines (565 loc) • 24.3 kB
JavaScript
import _toPropertyKey from "@babel/runtime/helpers/esm/toPropertyKey";
import _objectWithoutProperties from "@babel/runtime/helpers/esm/objectWithoutProperties";
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"],
_excluded2 = ["id"];
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, GridRowModes } from '../../../models/gridEditRowModel';
import { useGridApiMethod } from '../../utils/useGridApiMethod';
import { gridEditRowsStateSelector } from './gridEditingSelectors';
import { isPrintableKey } from '../../../utils/keyboardUtils';
import { gridColumnFieldsSelector, gridVisibleColumnFieldsSelector } from '../columns/gridColumnsSelector';
import { buildWarning } from '../../../utils/warning';
import { gridRowsDataRowIdToIdLookupSelector } from '../rows/gridRowsSelector';
import { deepClone } from '../../../utils/utils';
import { GridRowEditStopReasons, GridRowEditStartReasons } from '../../../models/params/gridRowParams';
import { GRID_ACTIONS_COLUMN_TYPE } from '../../../colDef';
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 useGridRowEditing = function useGridRowEditing(apiRef, props) {
var _React$useState = React.useState({}),
_React$useState2 = _slicedToArray(_React$useState, 2),
rowModesModel = _React$useState2[0],
setRowModesModel = _React$useState2[1];
var rowModesModelRef = React.useRef(rowModesModel);
var prevRowModesModel = React.useRef({});
var focusTimeout = React.useRef(null);
var nextFocusedCell = React.useRef(null);
var processRowUpdate = props.processRowUpdate,
onProcessRowUpdateError = props.onProcessRowUpdateError,
rowModesModelProp = props.rowModesModel,
onRowModesModelChange = props.onRowModesModelChange;
var runIfEditModeIsRow = function runIfEditModeIsRow(callback) {
return function () {
if (props.editMode === GridEditModes.Row) {
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, mode) {
if (apiRef.current.getRowMode(id) !== mode) {
throw new Error("MUI: The row with id=".concat(id, " is not in ").concat(mode, " mode."));
}
}, [apiRef]);
var handleCellDoubleClick = React.useCallback(function (params, event) {
if (!params.isEditable) {
return;
}
if (apiRef.current.getRowMode(params.id) === GridRowModes.Edit) {
return;
}
var rowParams = apiRef.current.getRowParams(params.id);
var newParams = _extends({}, rowParams, {
field: params.field,
reason: GridRowEditStartReasons.cellDoubleClick
});
apiRef.current.publishEvent('rowEditStart', newParams, event);
}, [apiRef]);
var handleCellFocusIn = React.useCallback(function (params) {
nextFocusedCell.current = params;
}, []);
var handleCellFocusOut = React.useCallback(function (params, event) {
if (!params.isEditable) {
return;
}
if (apiRef.current.getRowMode(params.id) === GridRowModes.View) {
return;
}
// The mechanism to detect if we can stop editing a row is different from
// the cell editing. Instead of triggering it when clicking outside a cell,
// we must check if another cell in the same row was not clicked. To achieve
// that, first we keep track of all cells that gained focus. When a cell loses
// focus we check if the next cell that received focus is from a different row.
nextFocusedCell.current = null;
focusTimeout.current = setTimeout(function () {
var _nextFocusedCell$curr;
focusTimeout.current = null;
if (((_nextFocusedCell$curr = nextFocusedCell.current) == null ? void 0 : _nextFocusedCell$curr.id) !== params.id) {
// The row might have been deleted during the click
if (!apiRef.current.getRow(params.id)) {
return;
}
// The row may already changed its mode
if (apiRef.current.getRowMode(params.id) === GridRowModes.View) {
return;
}
var rowParams = apiRef.current.getRowParams(params.id);
var newParams = _extends({}, rowParams, {
field: params.field,
reason: GridRowEditStopReasons.rowFocusOut
});
apiRef.current.publishEvent('rowEditStop', newParams, event);
}
});
}, [apiRef]);
React.useEffect(function () {
return function () {
clearTimeout(focusTimeout.current);
};
}, []);
var handleCellKeyDown = React.useCallback(function (params, event) {
if (params.cellMode === GridRowModes.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 = GridRowEditStopReasons.escapeKeyDown;
} else if (event.key === 'Enter') {
reason = GridRowEditStopReasons.enterKeyDown;
} else if (event.key === 'Tab') {
var columnFields = gridVisibleColumnFieldsSelector(apiRef).filter(function (field) {
var column = apiRef.current.getColumn(field);
if (column.type === GRID_ACTIONS_COLUMN_TYPE) {
return true;
}
return apiRef.current.isCellEditable(apiRef.current.getCellParams(params.id, field));
});
if (event.shiftKey) {
if (params.field === columnFields[0]) {
// Exit if user pressed Shift+Tab on the first field
reason = GridRowEditStopReasons.shiftTabKeyDown;
}
} else if (params.field === columnFields[columnFields.length - 1]) {
// Exit if user pressed Tab on the last field
reason = GridRowEditStopReasons.tabKeyDown;
}
// Always prevent going to the next element in the tab sequence because the focus is
// handled manually to support edit components rendered inside Portals
event.preventDefault();
if (!reason) {
var index = columnFields.findIndex(function (field) {
return field === params.field;
});
var nextFieldToFocus = columnFields[event.shiftKey ? index - 1 : index + 1];
apiRef.current.setCellFocus(params.id, nextFieldToFocus);
}
}
if (reason) {
var newParams = _extends({}, apiRef.current.getRowParams(params.id), {
reason: reason,
field: params.field
});
apiRef.current.publishEvent('rowEditStop', newParams, event);
}
} else if (params.isEditable) {
var _reason;
var canStartEditing = apiRef.current.unstable_applyPipeProcessors('canStartEditing', true, {
event: event,
cellParams: params,
editMode: 'row'
});
if (!canStartEditing) {
return;
}
if (isPrintableKey(event)) {
_reason = GridRowEditStartReasons.printableKeyDown;
} else if ((event.ctrlKey || event.metaKey) && event.key === 'v') {
_reason = GridRowEditStartReasons.printableKeyDown;
} else if (event.key === 'Enter') {
_reason = GridRowEditStartReasons.enterKeyDown;
} else if (event.key === 'Delete' || event.key === 'Backspace') {
// Delete on Windows, Backspace on macOS
_reason = GridRowEditStartReasons.deleteKeyDown;
}
if (_reason) {
var rowParams = apiRef.current.getRowParams(params.id);
var _newParams = _extends({}, rowParams, {
field: params.field,
reason: _reason
});
apiRef.current.publishEvent('rowEditStart', _newParams, event);
}
}
}, [apiRef]);
var handleRowEditStart = React.useCallback(function (params) {
var id = params.id,
field = params.field,
reason = params.reason;
var startRowEditModeParams = {
id: id,
fieldToFocus: field
};
if (reason === GridRowEditStartReasons.printableKeyDown || reason === GridRowEditStartReasons.deleteKeyDown) {
startRowEditModeParams.deleteValue = !!field;
}
apiRef.current.startRowEditMode(startRowEditModeParams);
}, [apiRef]);
var handleRowEditStop = React.useCallback(function (params) {
var id = params.id,
reason = params.reason,
field = params.field;
apiRef.current.runPendingEditCellValueMutation(id);
var cellToFocusAfter;
if (reason === GridRowEditStopReasons.enterKeyDown) {
cellToFocusAfter = 'below';
} else if (reason === GridRowEditStopReasons.tabKeyDown) {
cellToFocusAfter = 'right';
} else if (reason === GridRowEditStopReasons.shiftTabKeyDown) {
cellToFocusAfter = 'left';
}
var ignoreModifications = reason === 'escapeKeyDown';
apiRef.current.stopRowEditMode({
id: id,
ignoreModifications: ignoreModifications,
field: field,
cellToFocusAfter: cellToFocusAfter
});
}, [apiRef]);
useGridApiEventHandler(apiRef, 'cellDoubleClick', runIfEditModeIsRow(handleCellDoubleClick));
useGridApiEventHandler(apiRef, 'cellFocusIn', runIfEditModeIsRow(handleCellFocusIn));
useGridApiEventHandler(apiRef, 'cellFocusOut', runIfEditModeIsRow(handleCellFocusOut));
useGridApiEventHandler(apiRef, 'cellKeyDown', runIfEditModeIsRow(handleCellKeyDown));
useGridApiEventHandler(apiRef, 'rowEditStart', runIfEditModeIsRow(handleRowEditStart));
useGridApiEventHandler(apiRef, 'rowEditStop', runIfEditModeIsRow(handleRowEditStop));
useGridApiOptionHandler(apiRef, 'rowEditStart', props.onRowEditStart);
useGridApiOptionHandler(apiRef, 'rowEditStop', props.onRowEditStop);
var getRowMode = React.useCallback(function (id) {
if (props.editMode === GridEditModes.Cell) {
return GridRowModes.View;
}
var editingState = gridEditRowsStateSelector(apiRef.current.state);
var isEditing = editingState[id] && Object.keys(editingState[id]).length > 0;
return isEditing ? GridRowModes.Edit : GridRowModes.View;
}, [apiRef, props.editMode]);
var updateRowModesModel = useEventCallback(function (newModel) {
var isNewModelDifferentFromProp = newModel !== props.rowModesModel;
if (onRowModesModelChange && isNewModelDifferentFromProp) {
onRowModesModelChange(newModel, {});
}
if (props.rowModesModel && isNewModelDifferentFromProp) {
return; // The prop always win
}
setRowModesModel(newModel);
rowModesModelRef.current = newModel;
apiRef.current.publishEvent('rowModesModelChange', newModel);
});
var updateRowInRowModesModel = React.useCallback(function (id, newProps) {
var newModel = _extends({}, rowModesModelRef.current);
if (newProps !== null) {
newModel[id] = _extends({}, newProps);
} else {
delete newModel[id];
}
updateRowModesModel(newModel);
}, [updateRowModesModel]);
var updateOrDeleteRowState = React.useCallback(function (id, newProps) {
apiRef.current.setState(function (state) {
var newEditingState = _extends({}, state.editRows);
if (newProps !== null) {
newEditingState[id] = newProps;
} else {
delete newEditingState[id];
}
return _extends({}, state, {
editRows: newEditingState
});
});
apiRef.current.forceUpdate();
}, [apiRef]);
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 startRowEditMode = React.useCallback(function (params) {
var id = params.id,
other = _objectWithoutProperties(params, _excluded);
throwIfNotInMode(id, GridRowModes.View);
updateRowInRowModesModel(id, _extends({
mode: GridRowModes.Edit
}, other));
}, [throwIfNotInMode, updateRowInRowModesModel]);
var updateStateToStartRowEditMode = useEventCallback(function (params) {
var id = params.id,
fieldToFocus = params.fieldToFocus,
deleteValue = params.deleteValue,
initialValue = params.initialValue;
var columnFields = gridColumnFieldsSelector(apiRef);
var newProps = columnFields.reduce(function (acc, field) {
var cellParams = apiRef.current.getCellParams(id, field);
if (!cellParams.isEditable) {
return acc;
}
var newValue = apiRef.current.getCellValue(id, field);
if (fieldToFocus === field && (deleteValue || initialValue)) {
newValue = deleteValue ? '' : initialValue;
}
acc[field] = {
value: newValue,
error: false,
isProcessingProps: false
};
return acc;
}, {});
updateOrDeleteRowState(id, newProps);
if (fieldToFocus) {
apiRef.current.setCellFocus(id, fieldToFocus);
}
});
var stopRowEditMode = React.useCallback(function (params) {
var id = params.id,
other = _objectWithoutProperties(params, _excluded2);
throwIfNotInMode(id, GridRowModes.Edit);
updateRowInRowModesModel(id, _extends({
mode: GridRowModes.View
}, other));
}, [throwIfNotInMode, updateRowInRowModesModel]);
var updateStateToStopRowEditMode = useEventCallback(function (params) {
var id = params.id,
ignoreModifications = params.ignoreModifications,
focusedField = params.field,
_params$cellToFocusAf = params.cellToFocusAfter,
cellToFocusAfter = _params$cellToFocusAf === void 0 ? 'none' : _params$cellToFocusAf;
apiRef.current.runPendingEditCellValueMutation(id);
var finishRowEditMode = function finishRowEditMode() {
if (cellToFocusAfter !== 'none' && focusedField) {
apiRef.current.moveFocusToRelativeCell(id, focusedField, cellToFocusAfter);
}
updateOrDeleteRowState(id, null);
updateRowInRowModesModel(id, null);
};
if (ignoreModifications) {
finishRowEditMode();
return;
}
var editingState = gridEditRowsStateSelector(apiRef.current.state);
var row = apiRef.current.getRow(id);
var isSomeFieldProcessingProps = Object.values(editingState[id]).some(function (fieldProps) {
return fieldProps.isProcessingProps;
});
if (isSomeFieldProcessingProps) {
prevRowModesModel.current[id].mode = GridRowModes.Edit;
return;
}
var hasSomeFieldWithError = Object.values(editingState[id]).some(function (fieldProps) {
return fieldProps.error;
});
if (hasSomeFieldWithError) {
prevRowModesModel.current[id].mode = GridRowModes.Edit;
// Revert the mode in the rowModesModel prop back to "edit"
updateRowInRowModesModel(id, {
mode: GridRowModes.Edit
});
return;
}
var rowUpdate = apiRef.current.getRowWithUpdatedValuesFromRowEditing(id);
if (processRowUpdate) {
var handleError = function handleError(errorThrown) {
prevRowModesModel.current[id].mode = GridRowModes.Edit;
// Revert the mode in the rowModesModel prop back to "edit"
updateRowInRowModesModel(id, {
mode: GridRowModes.Edit
});
if (onProcessRowUpdateError) {
onProcessRowUpdateError(errorThrown);
} else {
missingOnProcessRowUpdateErrorWarning();
}
};
try {
Promise.resolve(processRowUpdate(rowUpdate, row)).then(function (finalRowUpdate) {
apiRef.current.updateRows([finalRowUpdate]);
finishRowEditMode();
}).catch(handleError);
} catch (errorThrown) {
handleError(errorThrown);
}
} else {
apiRef.current.updateRows([rowUpdate]);
finishRowEditMode();
}
});
var setRowEditingEditCellValue = React.useCallback(function (params) {
var id = params.id,
field = params.field,
value = params.value,
debounceMs = params.debounceMs,
skipValueParser = params.unstable_skipValueParser;
throwIfNotEditable(id, field);
var column = apiRef.current.getColumn(field);
var row = apiRef.current.getRow(id);
var parsedValue = value;
if (column.valueParser && !skipValueParser) {
parsedValue = column.valueParser(value, apiRef.current.getCellParams(id, field));
}
var editingState = gridEditRowsStateSelector(apiRef.current.state);
var newProps = _extends({}, editingState[id][field], {
value: parsedValue,
changeReason: debounceMs ? 'debouncedSetEditCellValue' : 'setEditCellValue'
});
if (!column.preProcessEditCellProps) {
updateOrDeleteFieldState(id, field, newProps);
}
return new Promise(function (resolve) {
var promises = [];
if (column.preProcessEditCellProps) {
var hasChanged = newProps.value !== editingState[id][field].value;
newProps = _extends({}, newProps, {
isProcessingProps: true
});
updateOrDeleteFieldState(id, field, newProps);
var _editingState$id = editingState[id],
ignoredField = _editingState$id[field],
otherFieldsProps = _objectWithoutProperties(_editingState$id, [field].map(_toPropertyKey));
var promise = Promise.resolve(column.preProcessEditCellProps({
id: id,
row: row,
props: newProps,
hasChanged: hasChanged,
otherFieldsProps: otherFieldsProps
})).then(function (processedProps) {
// Check again if the row is in edit mode because the user may have
// discarded the changes while the props were being processed.
if (apiRef.current.getRowMode(id) === GridRowModes.View) {
resolve(false);
return;
}
editingState = gridEditRowsStateSelector(apiRef.current.state);
processedProps = _extends({}, processedProps, {
isProcessingProps: false
});
// We don't reuse the value 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.
processedProps.value = column.preProcessEditCellProps ? editingState[id][field].value : parsedValue;
updateOrDeleteFieldState(id, field, processedProps);
});
promises.push(promise);
}
Object.entries(editingState[id]).forEach(function (_ref) {
var _ref2 = _slicedToArray(_ref, 2),
thisField = _ref2[0],
fieldProps = _ref2[1];
if (thisField === field) {
return;
}
var fieldColumn = apiRef.current.getColumn(thisField);
if (!fieldColumn.preProcessEditCellProps) {
return;
}
fieldProps = _extends({}, fieldProps, {
isProcessingProps: true
});
updateOrDeleteFieldState(id, thisField, fieldProps);
editingState = gridEditRowsStateSelector(apiRef.current.state);
var _editingState$id2 = editingState[id],
ignoredField = _editingState$id2[thisField],
otherFieldsProps = _objectWithoutProperties(_editingState$id2, [thisField].map(_toPropertyKey));
var promise = Promise.resolve(fieldColumn.preProcessEditCellProps({
id: id,
row: row,
props: fieldProps,
hasChanged: false,
otherFieldsProps: otherFieldsProps
})).then(function (processedProps) {
// Check again if the row is in edit mode because the user may have
// discarded the changes while the props were being processed.
if (apiRef.current.getRowMode(id) === GridRowModes.View) {
resolve(false);
return;
}
processedProps = _extends({}, processedProps, {
isProcessingProps: false
});
updateOrDeleteFieldState(id, thisField, processedProps);
});
promises.push(promise);
});
Promise.all(promises).then(function () {
if (apiRef.current.getRowMode(id) === GridRowModes.Edit) {
editingState = gridEditRowsStateSelector(apiRef.current.state);
resolve(!editingState[id][field].error);
} else {
resolve(false);
}
});
});
}, [apiRef, throwIfNotEditable, updateOrDeleteFieldState]);
var getRowWithUpdatedValuesFromRowEditing = React.useCallback(function (id) {
var editingState = gridEditRowsStateSelector(apiRef.current.state);
var row = apiRef.current.getRow(id);
if (!editingState[id]) {
return apiRef.current.getRow(id);
}
var rowUpdate = _extends({}, row);
Object.entries(editingState[id]).forEach(function (_ref3) {
var _ref4 = _slicedToArray(_ref3, 2),
field = _ref4[0],
fieldProps = _ref4[1];
var column = apiRef.current.getColumn(field);
if (column.valueSetter) {
rowUpdate = column.valueSetter({
value: fieldProps.value,
row: rowUpdate
});
} else {
rowUpdate[field] = fieldProps.value;
}
});
return rowUpdate;
}, [apiRef]);
var editingApi = {
getRowMode: getRowMode,
startRowEditMode: startRowEditMode,
stopRowEditMode: stopRowEditMode
};
var editingPrivateApi = {
setRowEditingEditCellValue: setRowEditingEditCellValue,
getRowWithUpdatedValuesFromRowEditing: getRowWithUpdatedValuesFromRowEditing
};
useGridApiMethod(apiRef, editingApi, 'public');
useGridApiMethod(apiRef, editingPrivateApi, 'private');
React.useEffect(function () {
if (rowModesModelProp) {
updateRowModesModel(rowModesModelProp);
}
}, [rowModesModelProp, updateRowModesModel]);
// 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 updateStateToStopRowEditMode may change it later
var copyOfPrevRowModesModel = prevRowModesModel.current;
prevRowModesModel.current = deepClone(rowModesModel); // Do a deep-clone because the attributes might be changed later
Object.entries(rowModesModel).forEach(function (_ref5) {
var _copyOfPrevRowModesMo, _idToIdLookup$id;
var _ref6 = _slicedToArray(_ref5, 2),
id = _ref6[0],
params = _ref6[1];
var prevMode = ((_copyOfPrevRowModesMo = copyOfPrevRowModesModel[id]) == null ? void 0 : _copyOfPrevRowModesMo.mode) || GridRowModes.View;
var originalId = (_idToIdLookup$id = idToIdLookup[id]) != null ? _idToIdLookup$id : id;
if (params.mode === GridRowModes.Edit && prevMode === GridRowModes.View) {
updateStateToStartRowEditMode(_extends({
id: originalId
}, params));
} else if (params.mode === GridRowModes.View && prevMode === GridRowModes.Edit) {
updateStateToStopRowEditMode(_extends({
id: originalId
}, params));
}
});
}, [apiRef, rowModesModel, updateStateToStartRowEditMode, updateStateToStopRowEditMode]);
};