UNPKG

react-beautiful-dnd-next

Version:

Beautiful and accessible drag and drop for lists with React

245 lines (211 loc) 6.99 kB
// @flow // eslint-disable-next-line no-unused-vars import { Component } from 'react'; import { connect } from 'react-redux'; import memoizeOne from 'memoize-one'; import type { State, DroppableId, DraggableId, CompletedDrag, DraggableDimension, DimensionMap, TypeId, Critical, } from '../../types'; import type { MapProps, OwnProps, DefaultProps, Selector, DispatchProps, StateSnapshot, } from './droppable-types'; import Droppable from './droppable'; import isStrictEqual from '../is-strict-equal'; import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over'; import { updateViewportMaxScroll as updateViewportMaxScrollAction } from '../../state/action-creators'; import StoreContext from '../context/store-context'; import whatIsDraggedOverFromResult from '../../state/droppable/what-is-dragged-over-from-result'; const isMatchingType = (type: TypeId, critical: Critical): boolean => type === critical.droppable.type; const getDraggable = ( critical: Critical, dimensions: DimensionMap, ): DraggableDimension => dimensions.draggables[critical.draggable.id]; // Returning a function to ensure each // Droppable gets its own selector export const makeMapStateToProps = (): Selector => { const idle: MapProps = { placeholder: null, shouldAnimatePlaceholder: true, snapshot: { isDraggingOver: false, draggingOverWith: null, draggingFromThisWith: null, }, }; const idleWithoutAnimation = { ...idle, shouldAnimatePlaceholder: false, }; const getMapProps = memoizeOne( ( id: DroppableId, isDraggingOver: boolean, dragging: DraggableDimension, snapshot: StateSnapshot, ): MapProps => { const isHome: boolean = dragging.descriptor.droppableId === id; if (isHome) { return { placeholder: dragging.placeholder, shouldAnimatePlaceholder: false, snapshot, }; } // not over foreign list - return idle if (!isDraggingOver) { return idle; } return { placeholder: dragging.placeholder, // Animating placeholder in foreign list shouldAnimatePlaceholder: true, snapshot, }; }, ); const getSnapshot = memoizeOne( ( id: DroppableId, isDraggingOver: boolean, dragging: DraggableDimension, ): StateSnapshot => { const draggableId: DraggableId = dragging.descriptor.id; const isHome: boolean = dragging.descriptor.droppableId === id; const draggingOverWith: ?DraggableId = isDraggingOver ? draggableId : null; const draggingFromThisWith: ?DraggableId = isHome ? draggableId : null; return { isDraggingOver, draggingOverWith, draggingFromThisWith, }; }, ); const selector = (state: State, ownProps: OwnProps): MapProps => { // not checking if item is disabled as we need the home list to display a placeholder const id: DroppableId = ownProps.droppableId; const type: TypeId = ownProps.type; if (state.isDragging) { const critical: Critical = state.critical; if (!isMatchingType(type, critical)) { return idle; } const dragging: DraggableDimension = getDraggable( critical, state.dimensions, ); const isDraggingOver: boolean = whatIsDraggedOver(state.impact) === id; // Snapshot based on current impact const snapshot: StateSnapshot = getSnapshot(id, isDraggingOver, dragging); return getMapProps(id, isDraggingOver, dragging, snapshot); } if (state.phase === 'DROP_ANIMATING') { const completed: CompletedDrag = state.completed; if (!isMatchingType(type, completed.critical)) { return idle; } const dragging: DraggableDimension = getDraggable( completed.critical, state.dimensions, ); // Snapshot based on result and not impact // The result might be null (cancel) but the impact is populated // to move everything back const snapshot: StateSnapshot = getSnapshot( id, whatIsDraggedOverFromResult(completed.result) === id, dragging, ); return getMapProps( id, whatIsDraggedOver(completed.impact) === id, dragging, snapshot, ); } // An error occurred and we need to clear everything // TODO: validate and add test if (state.phase === 'IDLE' && !state.completed && state.shouldFlush) { return idleWithoutAnimation; } if (state.phase === 'IDLE' && state.completed) { const completed: CompletedDrag = state.completed; if (!isMatchingType(type, completed.critical)) { return idle; } // Looking at impact as this controls the placeholder const wasOver: boolean = whatIsDraggedOver(completed.impact) === id; const wasCombining: boolean = Boolean(completed.impact.merge); // need to cut any animations: sadly a memoization fail // we need to do this for all lists as there might be // lists that are still animating a placeholder closed if (state.shouldFlush) { return idleWithoutAnimation; } if (wasOver) { // if reordering we need to cut an animation immediately // if merging: animate placeholder closed after drop return wasCombining ? idle : idleWithoutAnimation; } // keep default value return idle; } return idle; }; return selector; }; const mapDispatchToProps: DispatchProps = { updateViewportMaxScroll: updateViewportMaxScrollAction, }; const defaultProps = ({ type: 'DEFAULT', direction: 'vertical', isDropDisabled: false, isCombineEnabled: false, ignoreContainerClipping: false, }: DefaultProps); // Abstract class allows to specify props and defaults to component. // All other ways give any or do not let add default props. // eslint-disable-next-line /*:: class DroppableType extends Component<OwnProps> { static defaultProps = defaultProps; } */ // Leaning heavily on the default shallow equality checking // that `connect` provides. // It avoids needing to do it own within `Droppable` const ConnectedDroppable: typeof DroppableType = (connect( // returning a function so each component can do its own memoization makeMapStateToProps, // no dispatch props for droppable mapDispatchToProps, // mergeProps - using default null, { // Ensuring our context does not clash with consumers context: StoreContext, // pure: true is default value, but being really clear pure: true, // When pure, compares the result of mapStateToProps to its previous value. // Default value: shallowEqual // Switching to a strictEqual as we return a memoized object on changes areStatePropsEqual: isStrictEqual, }, ): any)(Droppable); ConnectedDroppable.defaultProps = defaultProps; export default ConnectedDroppable;