import { doesDropProduceChange, getIndexAfterDrop, getSortedChildren, buildParentLookup, } from '../data-tools' import type { CollisionDetails } from '../functional/grid-drag-controller/collision' import type { GridRowDropInfo, GridRowId } from '../types' import type { ApiSection } from './types' export type GridDragApi = { start: (rowId: GridRowId) => void move: (collisionDetails: CollisionDetails) => void complete: () => void cancel: () => void } const REMOVE_RECENTLY_DRAGGED_DELAY = 2000 export const createDragApi: ApiSection = (store, events) => ({ start(rowId) { const state = store.getState() const selection = store.selectors.selectSelection(state) const multiple = store.selectors.selectCanDragMultiple(state) let ids = multiple && selection.size && selection.has(rowId) ? new Set([rowId, ...selection]) : new Set([rowId]) if (ids.size > 1) { // Remove any descendants from the selection before starting the drag const childIdSet = store.selectors.selectAllDescendantsForIds( state, [...ids] ) ids = new Set([...ids].filter((id) => !childIdSet.has(id))) } /* Determine if we need to collapse the rows we are dragging */ const expandedRows = store.selectors.selectExpandedRows(state) const newCollapsed = new Set( [...expandedRows].filter((expandedId) => ids.has(expandedId)) ) if (newCollapsed.size) { const expandedResult = new Set( [...expandedRows].filter( (expandedId) => !newCollapsed.has(expandedId) ) ) /* Not a fan of this timer, would like to figure out a way to avoid this */ setTimeout(() => { events.emit('onExpandedRowsChange', expandedResult, { expanded: new Set(), collapsed: newCollapsed, }) }, 50) } store.dispatch({ type: 'startRowDrag', payload: { ids } }) }, move(collisionDetails) { const state = store.getState() const canDrop = store.selectors.selectDropPredicate(state) const draggingRowIds = store.selectors.selectDraggingRowIds(state) let targetParent: null | { row: any; rowMeta: any } = null function buildRow(id: GridRowId) { return { row: store.selectors.selectRow(state, id), rowMeta: store.selectors.selectRowMeta(state, id), } } // Maintain original order const sortedDraggingRowIds = store.selectors .selectRowIds(state) .filter((id) => draggingRowIds.has(id)) const draggedRows = [...sortedDraggingRowIds].map(buildRow) const { parentId } = collisionDetails if (parentId !== null) { targetParent = buildRow(parentId) } let targetIndex: number | null = null let resultIds: GridRowId[] | null = null if (collisionDetails.operation === 'between') { targetIndex = getIndexAfterDrop( store.selectors.selectRowCollection(state), draggingRowIds, collisionDetails.parentId, collisionDetails.prevInsertId, store.selectors.selectSortFn(state) ) if (targetIndex != null) { const rowCollection = store.selectors.selectRowCollection(state) const childrenIds = parentId !== null ? rowCollection.meta.get(parentId)?.children ?? [] : rowCollection.ids resultIds = childrenIds.filter((id) => !draggingRowIds.has(id)) resultIds.splice(targetIndex, 0, ...sortedDraggingRowIds) } } const canDropResult = canDrop({ draggedRowIds: sortedDraggingRowIds, draggedRows, targetParentId: collisionDetails.parentId, targetParent, targetIndex, droppedAfterId: collisionDetails.prevInsertId, resultIds, }) store.dispatch({ type: 'updateRowDrag', payload: { ...collisionDetails, message: '', ...canDropResult, }, }) }, complete() { const state = store.getState() const allowed = store.selectors.selectDropIsAllowed(state) const moveReceived = store.selectors.selectDragMoveReceived(state) if (!allowed || !moveReceived) { store.dispatch({ type: 'cancelRowDrag', payload: null, }) return } const mode = store.selectors.selectDragMode(state) const draggingRowIds = store.selectors.selectDraggingRowIds(state) const { parentId, prevInsertId, operation } = store.selectors.selectDragCollisionDetails(state) // Maintain original order const rowIds = store.selectors.selectRowIds(state) const sortedDraggingRowIds = rowIds.filter((id) => draggingRowIds.has(id) ) let targetIndex: number | null = null let resultIds: GridRowId[] | null = null let existingIds: GridRowId[] | null = null const rowCollection = store.selectors.selectRowCollection(state) const { parentLookup } = buildParentLookup(rowCollection) if (operation === 'between') { const sortFn = store.selectors.selectSortFn(state) targetIndex = getIndexAfterDrop( rowCollection, draggingRowIds, parentId, prevInsertId, sortFn ) existingIds = getSortedChildren(rowCollection, parentId, sortFn) if (targetIndex != null) { const childrenIds = parentId !== null ? rowCollection.meta.get(parentId)?.children ?? [] : rowCollection.ids resultIds = childrenIds.filter((id) => !draggingRowIds.has(id)) resultIds.splice(targetIndex, 0, ...sortedDraggingRowIds) } } let targetParent: null | { row: any; rowMeta: any } = null function buildRow(id: GridRowId) { return { row: store.selectors.selectRow(state, id), rowMeta: store.selectors.selectRowMeta(state, id), } } const draggedRows = [...sortedDraggingRowIds].map(buildRow) if (parentId !== null) { targetParent = buildRow(parentId) } const payload: GridRowDropInfo = { draggedRowIds: sortedDraggingRowIds, draggedRows, droppedAfterId: prevInsertId, targetParentId: parentId, targetParent, targetIndex, resultIds, } const dropProducesChange = doesDropProduceChange( payload, mode, parentLookup, existingIds ) if (!dropProducesChange) { store.dispatch({ type: 'cancelRowDrag', payload: null, }) return } events.emit('onRowDrop', payload) store.dispatch({ type: 'completeRowDrag', payload: null, }) setTimeout(() => { const state = store.getState() const recentlyDroppedRowIds = store.selectors.selectRecentlyDroppedRowIds(state) if (recentlyDroppedRowIds === draggingRowIds) { store.dispatch({ type: 'removeRecentlyDragged', payload: null, }) } }, REMOVE_RECENTLY_DRAGGED_DELAY) }, cancel() { store.dispatch({ type: 'cancelRowDrag', payload: null, }) }, })