react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
245 lines (211 loc) • 6.99 kB
JavaScript
// @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;