react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
269 lines (241 loc) • 7.43 kB
JavaScript
// @flow
import { type Position } from 'css-box-model';
import { type Node } from 'react';
import memoizeOne from 'memoize-one';
import { connect } from 'react-redux';
import Draggable from './draggable';
import { storeKey } from '../context-keys';
import { negate, origin } from '../../state/position';
import isStrictEqual from '../is-strict-equal';
import getDisplacementMap, {
type DisplacementMap,
} from '../../state/get-displacement-map';
import {
lift as liftAction,
move as moveAction,
moveUp as moveUpAction,
moveDown as moveDownAction,
moveLeft as moveLeftAction,
moveRight as moveRightAction,
drop as dropAction,
dropAnimationFinished as dropAnimationFinishedAction,
moveByWindowScroll as moveByWindowScrollAction,
} from '../../state/action-creators';
import type {
State,
DraggableId,
DroppableId,
DragMovement,
DraggableDimension,
Displacement,
PendingDrop,
} from '../../types';
import type {
MapProps,
OwnProps,
DefaultProps,
DispatchProps,
Selector,
} from './draggable-types';
const defaultMapProps: MapProps = {
isDropAnimating: false,
isDragging: false,
offset: origin,
shouldAnimateDragMovement: false,
// This is set to true by default so that as soon as Draggable
// needs to be displaced it can without needing to change this flag
shouldAnimateDisplacement: true,
// these properties are only populated when the item is dragging
dimension: null,
draggingOver: null,
};
// Returning a function to ensure each
// Draggable gets its own selector
export const makeMapStateToProps = (): Selector => {
const memoizedOffset = memoizeOne(
(x: number, y: number): Position => ({ x, y }),
);
const getNotDraggingProps = memoizeOne(
(offset: Position, shouldAnimateDisplacement: boolean): MapProps => ({
isDropAnimating: false,
isDragging: false,
offset,
shouldAnimateDisplacement,
// not relevant
shouldAnimateDragMovement: false,
dimension: null,
draggingOver: null,
}),
);
const getDraggingProps = memoizeOne(
(
offset: Position,
shouldAnimateDragMovement: boolean,
dimension: DraggableDimension,
// the id of the droppable you are over
draggingOver: ?DroppableId,
): MapProps => ({
isDragging: true,
isDropAnimating: false,
shouldAnimateDisplacement: false,
offset,
shouldAnimateDragMovement,
dimension,
draggingOver,
}),
);
const getOutOfTheWayMovement = (
id: DraggableId,
movement: DragMovement,
): ?MapProps => {
// Doing this cuts 50% of the time to move
// Otherwise need to loop over every item in every selector (yuck!)
const map: DisplacementMap = getDisplacementMap(movement.displaced);
const displacement: ?Displacement = map[id];
// does not need to move
if (!displacement) {
return null;
}
// do not need to do anything
if (!displacement.isVisible) {
return null;
}
const amount: Position = movement.isBeyondStartPosition
? negate(movement.amount)
: movement.amount;
return getNotDraggingProps(
memoizedOffset(amount.x, amount.y),
displacement.shouldAnimate,
);
};
const draggingSelector = (state: State, ownProps: OwnProps): ?MapProps => {
// Dragging
if (state.isDragging) {
// not the dragging item
if (state.critical.draggable.id !== ownProps.draggableId) {
return null;
}
const offset: Position = state.current.client.offset;
const dimension: DraggableDimension =
state.dimensions.draggables[ownProps.draggableId];
const shouldAnimateDragMovement: boolean = state.shouldAnimate;
const draggingOver: ?DroppableId = state.impact.destination
? state.impact.destination.droppableId
: null;
return getDraggingProps(
memoizedOffset(offset.x, offset.y),
shouldAnimateDragMovement,
dimension,
draggingOver,
);
}
// Dropping
if (state.phase === 'DROP_ANIMATING') {
const pending: PendingDrop = state.pending;
if (pending.result.draggableId !== ownProps.draggableId) {
return null;
}
const draggingOver: ?DroppableId = pending.result.destination
? pending.result.destination.droppableId
: null;
// not memoized as it is the only execution
return {
isDragging: false,
isDropAnimating: true,
offset: pending.newHomeOffset,
// still need to provide the dimension for the placeholder
dimension: state.dimensions.draggables[ownProps.draggableId],
draggingOver,
// animation will be controlled by the isDropAnimating flag
shouldAnimateDragMovement: false,
// not relevant,
shouldAnimateDisplacement: false,
};
}
return null;
};
const movingOutOfTheWaySelector = (
state: State,
ownProps: OwnProps,
): ?MapProps => {
// Dragging
if (state.isDragging) {
// we do not care about the dragging item
if (state.critical.draggable.id === ownProps.draggableId) {
return null;
}
return getOutOfTheWayMovement(
ownProps.draggableId,
state.impact.movement,
);
}
// Dropping
if (state.phase === 'DROP_ANIMATING') {
// do nothing if this was the dragging item
if (state.pending.result.draggableId === ownProps.draggableId) {
return null;
}
return getOutOfTheWayMovement(
ownProps.draggableId,
state.pending.impact.movement,
);
}
// Otherwise
return null;
};
const selector = (state: State, ownProps: OwnProps): MapProps => {
const dragging: ?MapProps = draggingSelector(state, ownProps);
if (dragging) {
return dragging;
}
const movingOutOfTheWay: ?MapProps = movingOutOfTheWaySelector(
state,
ownProps,
);
if (movingOutOfTheWay) {
return movingOutOfTheWay;
}
return defaultMapProps;
};
return selector;
};
const mapDispatchToProps: DispatchProps = {
lift: liftAction,
move: moveAction,
moveUp: moveUpAction,
moveDown: moveDownAction,
moveLeft: moveLeftAction,
moveRight: moveRightAction,
moveByWindowScroll: moveByWindowScrollAction,
drop: dropAction,
dropAnimationFinished: dropAnimationFinishedAction,
};
// Leaning heavily on the default shallow equality checking
// that `connect` provides.
// It avoids needing to do it own within `Draggable`
const ConnectedDraggable: OwnProps => Node = (connect(
// returning a function so each component can do its own memoization
makeMapStateToProps,
(mapDispatchToProps: any),
// mergeProps: use default
null,
// options
{
// Using our own store key.
// This allows consumers to also use redux
// Note: the default store key is 'store'
storeKey,
// 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)(Draggable);
ConnectedDraggable.defaultProps = ({
isDragDisabled: false,
// cannot drag interactive elements by default
disableInteractiveElementBlocking: false,
}: DefaultProps);
export default ConnectedDraggable;