UNPKG

react-beautiful-dnd-next

Version:

Beautiful and accessible drag and drop for lists with React

145 lines (125 loc) 4 kB
// @flow import invariant from 'tiny-invariant'; import type { Position } from 'css-box-model'; import type { State, DropReason, Critical, DraggableLocation, DropResult, CompletedDrag, Combine, DimensionMap, DraggableDimension, } from '../../../types'; import type { MiddlewareStore, Dispatch, Action } from '../../store-types'; import { animateDrop, completeDrop, dropPending, type AnimateDropArgs, } from '../../action-creators'; import { isEqual } from '../../position'; import getDropDuration from './get-drop-duration'; import getNewHomeClientOffset from './get-new-home-client-offset'; import getDropImpact, { type Result } from './get-drop-impact'; export default ({ getState, dispatch }: MiddlewareStore) => ( next: Dispatch, ) => (action: Action): any => { if (action.type !== 'DROP') { next(action); return; } const state: State = getState(); const reason: DropReason = action.payload.reason; // Still waiting for a bulk collection to publish // We are now shifting the application into the 'DROP_PENDING' phase if (state.phase === 'COLLECTING') { dispatch(dropPending({ reason })); return; } // Could have occurred in response to an error if (state.phase === 'IDLE') { return; } // Still waiting for our drop pending to end // TODO: should this throw? const isWaitingForDrop: boolean = state.phase === 'DROP_PENDING' && state.isWaiting; invariant( !isWaitingForDrop, 'A DROP action occurred while DROP_PENDING and still waiting', ); invariant( state.phase === 'DRAGGING' || state.phase === 'DROP_PENDING', `Cannot drop in phase: ${state.phase}`, ); // We are now in the DRAGGING or DROP_PENDING phase const critical: Critical = state.critical; const dimensions: DimensionMap = state.dimensions; // Only keeping impact when doing a user drop - otherwise we are cancelling const { impact, didDropInsideDroppable }: Result = getDropImpact({ reason, lastImpact: state.impact, onLift: state.onLift, onLiftImpact: state.onLiftImpact, home: state.dimensions.droppables[state.critical.droppable.id], viewport: state.viewport, draggables: state.dimensions.draggables, }); const draggable: DraggableDimension = dimensions.draggables[state.critical.draggable.id]; // only populating destination / combine if 'didDropInsideDroppable' is true const destination: ?DraggableLocation = didDropInsideDroppable ? impact.destination : null; const combine: ?Combine = didDropInsideDroppable && impact.merge ? impact.merge.combine : null; const source: DraggableLocation = { index: critical.draggable.index, droppableId: critical.droppable.id, }; const result: DropResult = { draggableId: draggable.descriptor.id, type: draggable.descriptor.type, source, reason, mode: state.movementMode, // destination / combine will be null if didDropInsideDroppable is true destination, combine, }; const newHomeClientOffset: Position = getNewHomeClientOffset({ impact, draggable, dimensions, viewport: state.viewport, onLift: state.onLift, }); const completed: CompletedDrag = { critical: state.critical, result, impact, }; const isAnimationRequired: boolean = // 1. not already in the right spot !isEqual(state.current.client.offset, newHomeClientOffset) || // 2. doing a combine (we still want to animate the scale and opacity fade) // looking at the result and not the impact as the combine impact is cleared Boolean(result.combine); if (!isAnimationRequired) { dispatch(completeDrop({ completed, shouldFlush: false })); return; } const dropDuration: number = getDropDuration({ current: state.current.client.offset, destination: newHomeClientOffset, reason, }); const args: AnimateDropArgs = { newHomeClientOffset, dropDuration, completed, }; dispatch(animateDrop(args)); };