UNPKG

@mui/x-data-grid-premium

Version:

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

303 lines (290 loc) 12 kB
"use strict"; 'use client'; var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard").default; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault").default; Object.defineProperty(exports, "__esModule", { value: true }); exports.useGridHistory = exports.historyStateInitializer = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var React = _interopRequireWildcard(require("react")); var _isObjectEmpty = require("@mui/x-internals/isObjectEmpty"); var _debounce = _interopRequireDefault(require("@mui/utils/debounce")); var _internals = require("@mui/x-data-grid-pro/internals"); var _gridHistorySelectors = require("./gridHistorySelectors"); var _defaultHistoryHandlers = require("./defaultHistoryHandlers"); const historyStateInitializer = state => { return (0, _extends2.default)({}, state, { history: { stack: [], currentPosition: -1, enabled: false } }); }; exports.historyStateInitializer = historyStateInitializer; const useGridHistory = (apiRef, props) => { const { historyStackSize, onUndo, onRedo, historyValidationEvents } = props; // Use default history events if none provided const historyEventHandlers = React.useMemo(() => { if (props.historyEventHandlers && !(0, _isObjectEmpty.isObjectEmpty)(props.historyEventHandlers)) { return props.historyEventHandlers; } return (0, _defaultHistoryHandlers.createDefaultHistoryHandlers)(apiRef, { dataSource: props.dataSource, columns: props.columns, isCellEditable: props.isCellEditable }); }, [apiRef, props.columns, props.isCellEditable, props.dataSource, props.historyEventHandlers]); const isEnabled = React.useMemo(() => historyStackSize > 0 && !(0, _isObjectEmpty.isObjectEmpty)(historyEventHandlers), [historyStackSize, historyEventHandlers]); const isValidationNeeded = React.useMemo(() => isEnabled && historyValidationEvents.length > 0 && Object.values(historyEventHandlers).some(handler => handler.validate), [isEnabled, historyEventHandlers, historyValidationEvents]); // Internal ref to track undo/redo operation state // - 'idle': everything is done // - 'in-progress': during async undo/redo handler execution (skip validation and prevent the state change by other events) // - 'waiting-replay': after undo/redo handler is done, the validation event is triggered again (as undo/redo is changing the state). // In this hook we want to skip the replayed event. const operationStateRef = React.useRef('idle'); // History event unsubscribers const eventUnsubscribersRef = React.useRef([]); // Validation event unsubscribers const validationEventUnsubscribersRef = React.useRef([]); const updateHistoryState = React.useCallback(newState => { apiRef.current.setState(state => (0, _extends2.default)({}, state, { history: (0, _extends2.default)({}, state.history, newState) })); }, [apiRef]); const addToStack = React.useCallback(item => { const currentPosition = (0, _gridHistorySelectors.gridHistoryCurrentPositionSelector)(apiRef); let newStack = [...(0, _gridHistorySelectors.gridHistoryStackSelector)(apiRef)]; // If we're not at the end of the stack, truncate forward history if (currentPosition < newStack.length - 1) { newStack = newStack.slice(0, currentPosition + 1); } // Add the new item newStack.push(item); // If stack exceeds size, remove oldest items if (newStack.length > historyStackSize) { newStack = newStack.slice(newStack.length - historyStackSize); } updateHistoryState({ stack: newStack, currentPosition: newStack.length - 1 }); }, [apiRef, updateHistoryState, historyStackSize]); const clear = React.useCallback(() => { updateHistoryState({ stack: [], currentPosition: -1 }); }, [updateHistoryState]); const clearUndoItems = React.useCallback(() => { const stack = (0, _gridHistorySelectors.gridHistoryStackSelector)(apiRef); const currentPosition = (0, _gridHistorySelectors.gridHistoryCurrentPositionSelector)(apiRef); // If we're at the end of the stack (no redo items), clear everything if (currentPosition >= stack.length - 1) { clear(); } else { updateHistoryState({ stack: stack.slice(currentPosition + 1), currentPosition: -1 }); } }, [apiRef, clear, updateHistoryState]); const clearRedoItems = React.useCallback(() => { const stack = (0, _gridHistorySelectors.gridHistoryStackSelector)(apiRef); const currentPosition = (0, _gridHistorySelectors.gridHistoryCurrentPositionSelector)(apiRef); updateHistoryState({ stack: stack.slice(0, currentPosition + 1) }); }, [apiRef, updateHistoryState]); const canUndo = React.useCallback(() => (0, _gridHistorySelectors.gridHistoryCanUndoSelector)(apiRef), [apiRef]); const canRedo = React.useCallback(() => (0, _gridHistorySelectors.gridHistoryCanRedoSelector)(apiRef), [apiRef]); const validateStackItems = React.useCallback(() => { /** * When: * - idle: continue with the validation * - in-progress: skip the validation and don't change the state * - waiting-replay: skip the validation this time and reset the state to idle */ if (operationStateRef.current !== 'idle') { if (operationStateRef.current === 'waiting-replay') { operationStateRef.current = 'idle'; } return; } const stack = (0, _gridHistorySelectors.gridHistoryStackSelector)(apiRef); const currentPosition = (0, _gridHistorySelectors.gridHistoryCurrentPositionSelector)(apiRef); if (historyStackSize === 0) { if (stack.length > 0) { clear(); } return; } if (stack.length === 0) { return; } const newStack = [...stack]; // Redo check if (currentPosition + 1 < newStack.length) { const item = newStack[currentPosition + 1]; const handler = historyEventHandlers[item.eventName]; if (!handler) { clearRedoItems(); } else { const isValid = handler.validate ? handler.validate(item.data, 'redo') : true; if (!isValid) { clearRedoItems(); } } } // Undo check if (currentPosition >= 0) { const item = newStack[currentPosition]; const handler = historyEventHandlers[item.eventName]; if (!handler) { clearUndoItems(); } else { const isValid = handler.validate ? handler.validate(item.data, 'undo') : true; if (!isValid) { clearUndoItems(); } } } }, [apiRef, historyEventHandlers, historyStackSize, clear, clearUndoItems, clearRedoItems]); const debouncedValidateStackItems = React.useMemo(() => (0, _debounce.default)(validateStackItems, 0), [validateStackItems]); const apply = React.useCallback(async (item, operation) => { const currentPosition = (0, _gridHistorySelectors.gridHistoryCurrentPositionSelector)(apiRef); const clearMethod = operation === 'undo' ? clearUndoItems : clearRedoItems; const { eventName, data } = item; const handler = historyEventHandlers[eventName]; if (!handler) { // If the handler is not found, it means tha we are updating the handlers map, so we can igore this request return false; } const isValid = handler.validate ? handler.validate(data, operation) : true; // The data is validated every time state change event happens. // We can get into a situation where the operation is not valid at this point only with the direct state updates. if (!isValid) { // Clear history and return false clearMethod(); return false; } // Execute the operation operationStateRef.current = 'in-progress'; await handler[operation](data); operationStateRef.current = 'waiting-replay'; updateHistoryState({ currentPosition: operation === 'undo' ? currentPosition - 1 : currentPosition + 1 }); apiRef.current.publishEvent(operation, { eventName, data }); // If there are no validations in the current setup, skip calling it and change the operation state to idle if (isValidationNeeded) { validateStackItems(); } else { operationStateRef.current = 'idle'; } return true; }, [apiRef, isValidationNeeded, historyEventHandlers, clearUndoItems, clearRedoItems, updateHistoryState, validateStackItems]); const undo = React.useCallback(async () => { if (!canUndo()) { return false; } const stack = (0, _gridHistorySelectors.gridHistoryStackSelector)(apiRef); const currentPosition = (0, _gridHistorySelectors.gridHistoryCurrentPositionSelector)(apiRef); return apply(stack[currentPosition], 'undo'); }, [apiRef, apply, canUndo]); const redo = React.useCallback(async () => { if (!canRedo()) { return false; } const stack = (0, _gridHistorySelectors.gridHistoryStackSelector)(apiRef); const currentPosition = (0, _gridHistorySelectors.gridHistoryCurrentPositionSelector)(apiRef); return apply(stack[currentPosition + 1], 'redo'); }, [apiRef, apply, canRedo]); const historyApi = { undo, redo, clear, canUndo, canRedo }; (0, _internals.useGridApiMethod)(apiRef, { history: historyApi }, 'public'); const handleKeyDown = React.useCallback(async event => { if (!(0, _internals.isUndoShortcut)(event) && !(0, _internals.isRedoShortcut)(event)) { return; } const action = (0, _internals.isUndoShortcut)(event) ? apiRef.current.history.undo : apiRef.current.history.redo; event.preventDefault(); event.stopPropagation(); await action(); }, [apiRef]); (0, _internals.useGridNativeEventListener)(apiRef, () => apiRef.current.rootElementRef.current, 'keydown', (0, _internals.runIf)(isEnabled, handleKeyDown)); (0, _internals.useGridEvent)(apiRef, 'undo', onUndo); (0, _internals.useGridEvent)(apiRef, 'redo', onRedo); React.useEffect(() => { updateHistoryState({ enabled: isEnabled }); }, [isEnabled, updateHistoryState]); React.useEffect(() => { if (!isValidationNeeded) { return () => {}; } historyValidationEvents.forEach(eventName => { validationEventUnsubscribersRef.current.push(apiRef.current.subscribeEvent(eventName, debouncedValidateStackItems)); }); return () => { validationEventUnsubscribersRef.current.forEach(unsubscribe => unsubscribe()); validationEventUnsubscribersRef.current = []; }; }, [apiRef, isValidationNeeded, historyValidationEvents, debouncedValidateStackItems]); React.useEffect(() => { if (historyStackSize === 0) { return () => {}; } const events = Object.keys(historyEventHandlers); // Subscribe to all events in the map events.forEach(eventName => { const handler = historyEventHandlers[eventName]; const unsubscribe = apiRef.current.subscribeEvent(eventName, (...params) => { // Don't store if the event was triggered by undo/redo if (operationStateRef.current !== 'idle') { return; } const data = handler.store(...params); if (data !== null) { addToStack({ eventName, data }); } }); eventUnsubscribersRef.current.push(unsubscribe); }); return () => { eventUnsubscribersRef.current.forEach(unsubscribe => unsubscribe()); eventUnsubscribersRef.current = []; }; }, [apiRef, historyEventHandlers, historyStackSize, addToStack]); // If the stack size is changed and it is smaller than the current stack size, clear the stack React.useEffect(() => { const currentStackSize = (0, _gridHistorySelectors.gridHistoryStackSelector)(apiRef).length; if (currentStackSize > historyStackSize) { clear(); } }, [apiRef, historyStackSize, clear]); }; exports.useGridHistory = useGridHistory;