react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
257 lines (223 loc) • 7.3 kB
Flow
// @flow
import memoizeOne from 'memoize-one';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import Draggable from './draggable';
import { storeKey } from '../context-keys';
import { negate } from '../../state/position';
import getDisplacementMap, { type DisplacementMap } from '../../state/get-displacement-map';
import {
lift as liftAction,
move as moveAction,
moveForward as moveForwardAction,
moveBackward as moveBackwardAction,
crossAxisMoveForward as crossAxisMoveForwardAction,
crossAxisMoveBackward as crossAxisMoveBackwardAction,
drop as dropAction,
cancel as cancelAction,
dropAnimationFinished as dropAnimationFinishedAction,
moveByWindowScroll as moveByWindowScrollAction,
} from '../../state/action-creators';
import type {
State,
Position,
DraggableId,
DragMovement,
DraggableDimension,
Direction,
Displacement,
} from '../../types';
import type {
MapProps,
OwnProps,
DispatchProps,
Selector,
} from './draggable-types';
const origin: Position = { x: 0, y: 0 };
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,
direction: null,
};
export const makeSelector = (): 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,
direction: null,
}),
);
const getDraggingProps = memoizeOne((
offset: Position,
shouldAnimateDragMovement: boolean,
dimension: DraggableDimension,
// direction of the droppable you are over
direction: ?Direction,
): MapProps => ({
isDragging: true,
isDropAnimating: false,
shouldAnimateDisplacement: false,
offset,
shouldAnimateDragMovement,
dimension,
direction,
}));
const draggingSelector = (state: State, ownProps: OwnProps): ?MapProps => {
if (state.phase !== 'DRAGGING' && state.phase !== 'DROP_ANIMATING') {
return null;
}
if (state.phase === 'DRAGGING') {
if (!state.drag) {
console.error('invalid drag state found in selector');
return null;
}
// not the dragging item
if (state.drag.initial.descriptor.id !== ownProps.draggableId) {
return null;
}
const offset: Position = state.drag.current.client.offset;
const dimension: DraggableDimension = state.dimension.draggable[ownProps.draggableId];
const direction: ?Direction = state.drag.impact.direction;
const shouldAnimateDragMovement: boolean = state.drag.current.shouldAnimate;
return getDraggingProps(
memoizedOffset(offset.x, offset.y),
shouldAnimateDragMovement,
dimension,
direction,
);
}
// dropping
if (!state.drop || !state.drop.pending) {
console.error('cannot provide props for dropping item when there is invalid state');
return null;
}
// this was not the dragging item
if (state.drop.pending.result.draggableId !== ownProps.draggableId) {
return null;
}
// not memoized as it is the only execution
return {
isDragging: false,
isDropAnimating: true,
offset: state.drop.pending.newHomeOffset,
// still need to provide the dimension for the placeholder
dimension: state.dimension.draggable[ownProps.draggableId],
// direction no longer needed as drag handle is unbound
direction: null,
// animation will be controlled by the isDropAnimating flag
shouldAnimateDragMovement: false,
// not relevant,
shouldAnimateDisplacement: false,
};
};
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 movingOutOfTheWaySelector = (state: State, ownProps: OwnProps): ?MapProps => {
if (state.phase !== 'DRAGGING' && state.phase !== 'DROP_ANIMATING') {
return null;
}
if (state.phase === 'DRAGGING') {
if (!state.drag) {
console.error('cannot correctly move item out of the way when there is invalid state');
return null;
}
// we do not care about the dragging item
if (state.drag.initial.descriptor.id === ownProps.draggableId) {
return null;
}
return getOutOfTheWayMovement(ownProps.draggableId, state.drag.impact.movement);
}
// state.phase === 'DROP_ANIMATING'
if (!state.drop || !state.drop.pending) {
console.error('cannot provide props for dropping item when there is invalid state');
return null;
}
// do nothing if this was the dragging item
if (state.drop.pending.result.draggableId === ownProps.draggableId) {
return null;
}
return getOutOfTheWayMovement(ownProps.draggableId, state.drop.pending.impact.movement);
};
return createSelector(
[
draggingSelector,
movingOutOfTheWaySelector,
],
(
dragging: ?MapProps,
movingOutOfTheWay: ?MapProps,
): MapProps => {
if (dragging) {
return dragging;
}
if (movingOutOfTheWay) {
return movingOutOfTheWay;
}
return defaultMapProps;
},
);
};
const makeMapStateToProps = () => {
const selector: Selector = makeSelector();
// $FlowFixMe - no idea how to type this correctly
return (state: State, props: OwnProps) => selector(state, props);
};
const mapDispatchToProps: DispatchProps = {
lift: liftAction,
move: moveAction,
moveForward: moveForwardAction,
moveBackward: moveBackwardAction,
crossAxisMoveForward: crossAxisMoveForwardAction,
crossAxisMoveBackward: crossAxisMoveBackwardAction,
moveByWindowScroll: moveByWindowScrollAction,
drop: dropAction,
dropAnimationFinished: dropAnimationFinishedAction,
cancel: cancelAction,
};
// Leaning heavily on the default shallow equality checking
// that `connect` provides.
// It avoids needing to do it own within `Draggable`
export default connect(
// returning a function to ensure each
// Draggable gets its own selector
makeMapStateToProps,
mapDispatchToProps,
null,
{ storeKey },
)(Draggable);