UNPKG

@mui/x-data-grid-pro

Version:

The Pro plan edition of the MUI X Data Grid components.

473 lines (456 loc) 19.5 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.useGridRowReorder = exports.rowReorderStateInitializer = void 0; var _extends2 = _interopRequireDefault(require("@babel/runtime/helpers/extends")); var React = _interopRequireWildcard(require("react")); var _useTimeout = _interopRequireDefault(require("@mui/utils/useTimeout")); var _composeClasses = _interopRequireDefault(require("@mui/utils/composeClasses")); var _xDataGrid = require("@mui/x-data-grid"); var _internals = require("@mui/x-data-grid/internals"); var _gridRowReorderColDef = require("./gridRowReorderColDef"); var _utils = require("./utils"); const EMPTY_REORDER_STATE = { previousTargetId: null, dragDirection: null, previousDropPosition: null }; const TIMEOUT_CLEAR_BUFFER_PX = 5; const EMPTY_TIMEOUT_INFO = { rowId: null }; const useUtilityClasses = ownerState => { const { classes } = ownerState; const slots = { rowDragging: ['row--dragging'], rowBeingDragged: ['row--beingDragged'] }; return (0, _composeClasses.default)(slots, _xDataGrid.getDataGridUtilityClass, classes); }; const rowReorderStateInitializer = state => (0, _extends2.default)({}, state, { rowReorder: { isActive: false } }); /** * Hook for row reordering (Pro package) * @requires useGridRows (method) */ exports.rowReorderStateInitializer = rowReorderStateInitializer; const useGridRowReorder = (apiRef, props) => { const logger = (0, _xDataGrid.useGridLogger)(apiRef, 'useGridRowReorder'); const sortModel = (0, _xDataGrid.useGridSelector)(apiRef, _xDataGrid.gridSortModelSelector); const dragRowNode = React.useRef(null); const originRowIndex = React.useRef(null); const removeDnDStylesTimeout = React.useRef(undefined); const ownerState = { classes: props.classes }; const classes = useUtilityClasses(ownerState); const [dragRowId, setDragRowId] = React.useState(''); const timeoutInfoRef = React.useRef(EMPTY_TIMEOUT_INFO); const timeout = (0, _useTimeout.default)(); const previousReorderState = React.useRef(EMPTY_REORDER_STATE); const dropTarget = React.useRef({ targetRowId: null, targetRowIndex: null, dropPosition: null }); React.useEffect(() => { return () => { clearTimeout(removeDnDStylesTimeout.current); }; }, []); // TODO: remove sortModel check once row reorder is sorting compatible const isRowReorderDisabled = React.useMemo(() => { return !props.rowReordering || !!sortModel.length; }, [props.rowReordering, sortModel]); const calculateDropPosition = React.useCallback(event => { // For tree data, we need to find the cell element to avoid flickerings on top 20% selection const targetElement = props.treeData ? (0, _utils.findCellElement)(event.target) : event.target; const targetRect = targetElement.getBoundingClientRect(); const relativeY = Math.floor(event.clientY - targetRect.top); if (props.treeData) { // For tree data: top 20% = above, middle 60% = over, bottom 20% = below const topThreshold = targetRect.height * 0.2; const bottomThreshold = targetRect.height * 0.8; if (relativeY < topThreshold) { return 'above'; } if (relativeY > bottomThreshold) { return 'below'; } return 'inside'; } // For flat data and row grouping: split at midpoint const midPoint = targetRect.height / 2; return relativeY < midPoint ? 'above' : 'below'; }, [props.treeData]); const applyDraggedState = React.useCallback((rowId, isDragged) => { if (rowId) { const draggedRow = apiRef.current.rootElementRef?.current?.querySelector(`[data-id="${rowId}"]`); if (draggedRow) { if (isDragged) { draggedRow.classList.add(classes.rowBeingDragged); } else { draggedRow.classList.remove(classes.rowBeingDragged); } } } }, [apiRef, classes.rowBeingDragged]); const applyRowAnimation = React.useCallback(async callback => { const rootElement = apiRef.current.rootElementRef?.current; if (!rootElement) { return; } const visibleRows = rootElement.querySelectorAll('[data-id]'); if (!visibleRows.length) { return; } const rowsArray = Array.from(visibleRows); const initialPositions = new Map(); rowsArray.forEach(row => { const rowId = row.getAttribute('data-id'); if (rowId) { initialPositions.set(rowId, row.getBoundingClientRect()); } }); await callback(); // Use `requestAnimationFrame` to ensure DOM has updated requestAnimationFrame(() => { const newRows = rootElement.querySelectorAll('[data-id]'); const animations = []; newRows.forEach(row => { const rowId = row.getAttribute('data-id'); if (!rowId) { return; } const prevRect = initialPositions.get(rowId); if (!prevRect) { return; } const currentRect = row.getBoundingClientRect(); const deltaY = prevRect.top - currentRect.top; if (Math.abs(deltaY) > 1) { const animation = row.animate([{ transform: `translateY(${deltaY}px)` }, { transform: 'translateY(0)' }], { duration: 200, easing: 'ease-in-out', fill: 'forwards' }); animations.push(animation); } }); if (animations.length > 0) { Promise.allSettled(animations.map(a => a.finished)).then(() => {}); } }); }, [apiRef]); const handleDragStart = React.useCallback((params, event) => { // Call the gridEditRowsStateSelector directly to avoid infnite loop const editRowsState = (0, _internals.gridEditRowsStateSelector)(apiRef); event.dataTransfer.effectAllowed = 'copy'; if (isRowReorderDisabled || Object.keys(editRowsState).length !== 0) { return; } if (timeoutInfoRef.current) { timeout.clear(); timeoutInfoRef.current = EMPTY_TIMEOUT_INFO; } logger.debug(`Start dragging row ${params.id}`); // Prevent drag events propagation. // For more information check here https://github.com/mui/mui-x/issues/2680. event.stopPropagation(); apiRef.current.setState(state => (0, _extends2.default)({}, state, { rowReorder: (0, _extends2.default)({}, state.rowReorder, { isActive: true, draggedRowId: params.id }) })); dragRowNode.current = event.currentTarget; // Apply cell-level dragging class to the drag handle dragRowNode.current.classList.add(classes.rowDragging); setDragRowId(params.id); // Apply the dragged state to the entire row applyDraggedState(params.id, true); removeDnDStylesTimeout.current = setTimeout(() => { dragRowNode.current.classList.remove(classes.rowDragging); }); const sortedRowIndexLookup = (0, _xDataGrid.gridExpandedSortedRowIndexLookupSelector)(apiRef); originRowIndex.current = sortedRowIndexLookup[params.id]; apiRef.current.setCellFocus(params.id, _gridRowReorderColDef.GRID_REORDER_COL_DEF.field); }, [apiRef, isRowReorderDisabled, logger, classes.rowDragging, applyDraggedState, timeout]); const handleDragOver = React.useCallback((params, event) => { if (dragRowId === '') { return; } const targetNode = (0, _xDataGrid.gridRowNodeSelector)(apiRef, params.id); const sourceNode = (0, _xDataGrid.gridRowNodeSelector)(apiRef, dragRowId); if (!sourceNode || !targetNode || targetNode.type === 'footer' || targetNode.type === 'pinnedRow' || !event.target) { return; } logger.debug(`Dragging over row ${params.id}`); event.preventDefault(); // Prevent drag events propagation. // For more information check here https://github.com/mui/mui-x/issues/2680. event.stopPropagation(); if (timeoutInfoRef.current && (timeoutInfoRef.current.rowId !== params.id || // Avoid accidental opening of node when the user is moving over a row event.clientY > timeoutInfoRef.current.clientY + TIMEOUT_CLEAR_BUFFER_PX || event.clientY < timeoutInfoRef.current.clientY - TIMEOUT_CLEAR_BUFFER_PX || event.clientX > timeoutInfoRef.current.clientX + TIMEOUT_CLEAR_BUFFER_PX || event.clientX < timeoutInfoRef.current.clientX - TIMEOUT_CLEAR_BUFFER_PX)) { timeout.clear(); timeoutInfoRef.current = EMPTY_TIMEOUT_INFO; } // Calculate drop position using new logic const dropPosition = calculateDropPosition(event); if (targetNode.type === 'group' && !targetNode.childrenExpanded && !timeoutInfoRef.current.rowId && targetNode.id !== sourceNode.id && (dropPosition === 'inside' || targetNode.depth < sourceNode.depth)) { timeout.start(500, () => { const rowNode = (0, _xDataGrid.gridRowNodeSelector)(apiRef, params.id); // TODO: Handle `dataSource` case with https://github.com/mui/mui-x/issues/18947 apiRef.current.setRowChildrenExpansion(params.id, !rowNode.childrenExpanded); }); timeoutInfoRef.current = { rowId: params.id, clientY: event.clientY, clientX: event.clientX }; return; } const sortedRowIndexLookup = (0, _xDataGrid.gridExpandedSortedRowIndexLookupSelector)(apiRef); const targetRowIndex = sortedRowIndexLookup[params.id]; const sourceRowIndex = sortedRowIndexLookup[dragRowId]; const currentReorderState = { dragDirection: targetRowIndex < sourceRowIndex ? 'up' : 'down', previousTargetId: params.id, previousDropPosition: dropPosition }; // Update visual indicator when dragging over a different row or position if (previousReorderState.current.previousTargetId !== params.id || previousReorderState.current.previousDropPosition !== dropPosition) { const isSameNode = targetRowIndex === sourceRowIndex; // Check if this is an adjacent position const isAdjacentPosition = dropPosition === 'above' && targetRowIndex === sourceRowIndex + 1 || dropPosition === 'below' && targetRowIndex === sourceRowIndex - 1; const isRowReorderValid = apiRef.current.unstable_applyPipeProcessors('isRowReorderValid', false, { sourceRowId: dragRowId, targetRowId: params.id, dropPosition, dragDirection: currentReorderState.dragDirection }); // Show drop indicator for valid drops OR adjacent positions OR same node if (isRowReorderValid || isAdjacentPosition || isSameNode) { dropTarget.current = { targetRowId: params.id, targetRowIndex, dropPosition }; // Update state with drop target apiRef.current.setState(state => (0, _extends2.default)({}, state, { rowReorder: (0, _extends2.default)({}, state.rowReorder, { dropTarget: { rowId: params.id, position: dropPosition } }) })); } else { // Clear indicators for invalid drops dropTarget.current = { targetRowId: null, targetRowIndex: null, dropPosition: null }; // Clear state drop target apiRef.current.setState(state => (0, _extends2.default)({}, state, { rowReorder: (0, _extends2.default)({}, state.rowReorder, { dropTarget: undefined }) })); } previousReorderState.current = currentReorderState; } // Render the native 'copy' cursor for additional visual feedback if (dropTarget.current.targetRowId === null) { event.dataTransfer.dropEffect = 'none'; } else { event.dataTransfer.dropEffect = 'copy'; } }, [dragRowId, apiRef, logger, timeout, calculateDropPosition]); const handleDragEnd = React.useCallback(async (_, event) => { // Call the gridEditRowsStateSelector directly to avoid infnite loop const editRowsState = (0, _internals.gridEditRowsStateSelector)(apiRef); if (dragRowId === '' || isRowReorderDisabled || Object.keys(editRowsState).length !== 0) { return; } if (timeoutInfoRef.current) { timeout.clear(); timeoutInfoRef.current = EMPTY_TIMEOUT_INFO; } logger.debug('End dragging row'); event.preventDefault(); // Prevent drag events propagation. // For more information check here https://github.com/mui/mui-x/issues/2680. event.stopPropagation(); clearTimeout(removeDnDStylesTimeout.current); dragRowNode.current = null; const dragDirection = previousReorderState.current.dragDirection; previousReorderState.current = EMPTY_REORDER_STATE; // Check if the row was dropped outside the grid. if (!event.dataTransfer || event.dataTransfer.dropEffect === 'none') { // Reset drop target state dropTarget.current = { targetRowId: null, targetRowIndex: null, dropPosition: null }; originRowIndex.current = null; setDragRowId(''); // Clear visual indicators and dragged state applyDraggedState(dragRowId, false); apiRef.current.setState(state => (0, _extends2.default)({}, state, { rowReorder: { isActive: false, draggedRowId: null } })); return; } if (dropTarget.current.targetRowIndex !== null && dropTarget.current.targetRowId !== null) { const isRowReorderValid = apiRef.current.unstable_applyPipeProcessors('isRowReorderValid', false, { sourceRowId: dragRowId, targetRowId: dropTarget.current.targetRowId, dropPosition: dropTarget.current.dropPosition, dragDirection: dragDirection }); if (isRowReorderValid) { try { const rowTree = (0, _xDataGrid.gridRowTreeSelector)(apiRef); const sourceNode = (0, _xDataGrid.gridRowNodeSelector)(apiRef, dragRowId); if (!sourceNode) { throw new Error(`MUI X: No row node found for id #${dragRowId}`); } // Calculate oldParent and oldIndex const oldParent = sourceNode.parent; const oldParentNode = rowTree[oldParent]; const oldIndexInParent = oldParentNode.children.indexOf(dragRowId) ?? originRowIndex.current; await applyRowAnimation(async () => { await apiRef.current.setRowPosition(dragRowId, dropTarget.current.targetRowId, dropTarget.current.dropPosition); const updatedTree = (0, _xDataGrid.gridRowTreeSelector)(apiRef); const updatedNode = updatedTree[dragRowId]; if (!updatedNode) { throw new Error(`MUI X: Row node for id #${dragRowId} not found after move`); } const newParent = updatedNode.parent; const newParentNode = updatedTree[newParent]; const newIndexInParent = newParentNode.children.indexOf(dragRowId); // Only emit event and clear state after successful reorder const rowOrderChangeParams = { row: apiRef.current.getRow(dragRowId), oldIndex: oldIndexInParent, targetIndex: newIndexInParent, oldParent: oldParent === _xDataGrid.GRID_ROOT_GROUP_ID ? null : oldParent, newParent: newParent === _xDataGrid.GRID_ROOT_GROUP_ID ? null : newParent }; applyDraggedState(dragRowId, false); apiRef.current.setState(state => (0, _extends2.default)({}, state, { rowReorder: { isActive: false, draggedRowId: null } })); apiRef.current.publishEvent('rowOrderChange', rowOrderChangeParams); }); } catch (error) { // Handle error: reset visual state but don't publish success event applyDraggedState(dragRowId, false); apiRef.current.setState(state => (0, _extends2.default)({}, state, { rowReorder: { isActive: false, draggedRowId: null } })); } } else { applyDraggedState(dragRowId, false); apiRef.current.setState(state => (0, _extends2.default)({}, state, { rowReorder: (0, _extends2.default)({}, state.rowReorder, { dropTarget: undefined }) })); } } // Reset drop target state dropTarget.current = { targetRowId: null, targetRowIndex: null, dropPosition: null }; setDragRowId(''); }, [apiRef, dragRowId, isRowReorderDisabled, logger, applyDraggedState, timeout, applyRowAnimation]); const isValidRowReorderProp = props.isValidRowReorder; const isRowReorderValid = React.useCallback((initialValue, { sourceRowId, targetRowId, dropPosition, dragDirection }) => { if ((0, _xDataGrid.gridRowMaximumTreeDepthSelector)(apiRef) > 1) { return initialValue; } const sortedRowIndexLookup = (0, _xDataGrid.gridExpandedSortedRowIndexLookupSelector)(apiRef); const targetRowIndex = sortedRowIndexLookup[targetRowId]; const sourceRowIndex = sortedRowIndexLookup[sourceRowId]; // Apply internal validation: check if this drop would result in no actual movement const isAdjacentNode = dropPosition === 'above' && targetRowIndex === sourceRowIndex + 1 || // dragging to immediately below (above next row) dropPosition === 'below' && targetRowIndex === sourceRowIndex - 1; // dragging to immediately above (below previous row) if (isAdjacentNode || sourceRowIndex === targetRowIndex) { return false; } // Internal validation passed, now apply additional user validation if provided if (isValidRowReorderProp) { const expandedSortedRowIds = (0, _xDataGrid.gridExpandedSortedRowIdsSelector)(apiRef); const rowTree = (0, _xDataGrid.gridRowTreeSelector)(apiRef); const sourceNode = rowTree[sourceRowId]; const targetNode = rowTree[targetRowId]; const prevNode = targetRowIndex > 0 ? rowTree[expandedSortedRowIds[targetRowIndex - 1]] : null; const nextNode = targetRowIndex < expandedSortedRowIds.length - 1 ? rowTree[expandedSortedRowIds[targetRowIndex + 1]] : null; const context = { apiRef, sourceNode, targetNode, prevNode, nextNode, dropPosition, dragDirection }; if (!isValidRowReorderProp(context)) { return false; } } return true; }, [apiRef, isValidRowReorderProp]); (0, _internals.useGridRegisterPipeProcessor)(apiRef, 'isRowReorderValid', isRowReorderValid); (0, _xDataGrid.useGridEvent)(apiRef, 'rowDragStart', handleDragStart); (0, _xDataGrid.useGridEvent)(apiRef, 'rowDragOver', handleDragOver); (0, _xDataGrid.useGridEvent)(apiRef, 'rowDragEnd', handleDragEnd); (0, _xDataGrid.useGridEvent)(apiRef, 'cellDragOver', handleDragOver); (0, _xDataGrid.useGridEventPriority)(apiRef, 'rowOrderChange', props.onRowOrderChange); const setRowDragActive = React.useCallback(isActive => { apiRef.current.setState(state => (0, _extends2.default)({}, state, { rowReorder: (0, _extends2.default)({}, state.rowReorder, { isActive }) })); }, [apiRef]); (0, _xDataGrid.useGridApiMethod)(apiRef, { setRowDragActive }, 'private'); }; exports.useGridRowReorder = useGridRowReorder;