react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
353 lines (318 loc) • 10.1 kB
JavaScript
// @flow
import { type Position } from 'css-box-model';
// eslint-disable-next-line
import { Component } from 'react';
import memoizeOne from 'memoize-one';
import { connect } from 'react-redux';
import Draggable from './draggable';
import { origin } from '../../state/position';
import isStrictEqual from '../is-strict-equal';
import { curves, combine } from '../../animation';
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,
CombineImpact,
Displacement,
CompletedDrag,
DragImpact,
DisplacementMap,
MovementMode,
DropResult,
} from '../../types';
import type {
MapProps,
OwnProps,
DefaultProps,
DispatchProps,
Selector,
StateSnapshot,
DropAnimation,
} from './draggable-types';
import whatIsDraggedOver from '../../state/droppable/what-is-dragged-over';
import StoreContext from '../context/store-context';
import whatIsDraggedOverFromResult from '../../state/droppable/what-is-dragged-over-from-result';
const getCombineWithFromResult = (result: DropResult): ?DraggableId => {
return result.combine ? result.combine.draggableId : null;
};
const getCombineWithFromImpact = (impact: DragImpact): ?DraggableId => {
return impact.merge ? impact.merge.combine.draggableId : null;
};
// Returning a function to ensure each
// Draggable gets its own selector
export const makeMapStateToProps = (): Selector => {
const getDraggingSnapshot = memoizeOne(
(
mode: MovementMode,
draggingOver: ?DroppableId,
combineWith: ?DraggableId,
dropping: ?DropAnimation,
): StateSnapshot => ({
isDragging: true,
isDropAnimating: Boolean(dropping),
dropAnimation: dropping,
mode,
draggingOver,
combineWith,
combineTargetFor: null,
}),
);
const getSecondarySnapshot = memoizeOne(
(combineTargetFor: ?DraggableId): StateSnapshot => ({
isDragging: false,
isDropAnimating: false,
dropAnimation: null,
mode: null,
draggingOver: null,
combineTargetFor,
combineWith: null,
}),
);
const defaultMapProps: MapProps = {
mapped: {
type: 'SECONDARY',
offset: origin,
combineTargetFor: null,
shouldAnimateDisplacement: true,
snapshot: getSecondarySnapshot(null),
},
};
const memoizedOffset = memoizeOne((x: number, y: number): Position => ({
x,
y,
}));
const getDraggingProps = memoizeOne((
offset: Position,
mode: MovementMode,
dimension: DraggableDimension,
// the id of the droppable you are over
draggingOver: ?DroppableId,
// the id of a draggable you are grouping with
combineWith: ?DraggableId,
forceShouldAnimate: ?boolean,
): MapProps => ({
mapped: {
type: 'DRAGGING',
dropping: null,
draggingOver,
combineWith,
mode,
offset,
dimension,
forceShouldAnimate,
snapshot: getDraggingSnapshot(mode, draggingOver, combineWith, null),
},
}));
const getSecondaryProps = memoizeOne(
(
offset: Position,
combineTargetFor: ?DraggableId = null,
shouldAnimateDisplacement: boolean,
): MapProps => ({
mapped: {
type: 'SECONDARY',
offset,
combineTargetFor,
shouldAnimateDisplacement,
snapshot: getSecondarySnapshot(combineTargetFor),
},
}),
);
const getSecondaryMovement = (
ownId: DraggableId,
draggingId: DraggableId,
impact: DragImpact,
): ?MapProps => {
// Doing this cuts 50% of the time to move
// Otherwise need to loop over every item in every selector (yuck!)
const map: DisplacementMap = impact.movement.map;
const displacement: ?Displacement = map[ownId];
const movement: DragMovement = impact.movement;
const merge: ?CombineImpact = impact.merge;
const isCombinedWith: boolean = Boolean(
merge && merge.combine.draggableId === ownId,
);
const displacedBy: Position = movement.displacedBy.point;
const offset: Position = memoizedOffset(displacedBy.x, displacedBy.y);
if (isCombinedWith) {
return getSecondaryProps(
displacement ? offset : origin,
draggingId,
displacement ? displacement.shouldAnimate : true,
);
}
// does not need to move
if (!displacement) {
return null;
}
// do not need to do anything
if (!displacement.isVisible) {
return null;
}
return getSecondaryProps(offset, null, 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 mode: MovementMode = state.movementMode;
const draggingOver: ?DroppableId = whatIsDraggedOver(state.impact);
const combineWith: ?DraggableId = getCombineWithFromImpact(state.impact);
const forceShouldAnimate: ?boolean = state.forceShouldAnimate;
return getDraggingProps(
memoizedOffset(offset.x, offset.y),
mode,
dimension,
draggingOver,
combineWith,
forceShouldAnimate,
);
}
// Dropping
if (state.phase === 'DROP_ANIMATING') {
const completed: CompletedDrag = state.completed;
if (completed.result.draggableId !== ownProps.draggableId) {
return null;
}
const dimension: DraggableDimension =
state.dimensions.draggables[ownProps.draggableId];
const result: DropResult = completed.result;
const mode: MovementMode = result.mode;
// these need to be pulled from the result as they can be different to the final impact
const draggingOver: ?DroppableId = whatIsDraggedOverFromResult(result);
const combineWith: ?DraggableId = getCombineWithFromResult(result);
const duration: number = state.dropDuration;
// not memoized as it is the only execution
const dropping: DropAnimation = {
duration,
curve: curves.drop,
moveTo: state.newHomeClientOffset,
opacity: combineWith ? combine.opacity.drop : null,
scale: combineWith ? combine.scale.drop : null,
};
return {
mapped: {
type: 'DRAGGING',
offset: state.newHomeClientOffset,
dimension,
dropping,
draggingOver,
combineWith,
mode,
forceShouldAnimate: null,
snapshot: getDraggingSnapshot(
mode,
draggingOver,
combineWith,
dropping,
),
},
};
}
return null;
};
const secondarySelector = (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 getSecondaryMovement(
ownProps.draggableId,
state.critical.draggable.id,
state.impact,
);
}
// Dropping
if (state.phase === 'DROP_ANIMATING') {
const completed: CompletedDrag = state.completed;
// do nothing if this was the dragging item
if (completed.result.draggableId === ownProps.draggableId) {
return null;
}
return getSecondaryMovement(
ownProps.draggableId,
completed.result.draggableId,
completed.impact,
);
}
// Otherwise
return null;
};
const selector = (state: State, ownProps: OwnProps): MapProps =>
draggingSelector(state, ownProps) ||
secondarySelector(state, ownProps) ||
defaultMapProps;
return selector;
};
const mapDispatchToProps: DispatchProps = {
lift: liftAction,
move: moveAction,
moveUp: moveUpAction,
moveDown: moveDownAction,
moveLeft: moveLeftAction,
moveRight: moveRightAction,
moveByWindowScroll: moveByWindowScrollAction,
drop: dropAction,
dropAnimationFinished: dropAnimationFinishedAction,
};
const defaultProps = ({
isDragDisabled: false,
// Cannot drag interactive elements by default
disableInteractiveElementBlocking: false,
// Not respecting browser force touch interaction
// by default for a more consistent experience
shouldRespectForcePress: 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 DraggableType 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 `Draggable`
const ConnectedDraggable: typeof DraggableType = (connect(
// returning a function so each component can do its own memoization
makeMapStateToProps,
mapDispatchToProps,
// mergeProps: use default
null,
// options
{
// Using our own context for the store to avoid clashing with consumers
context: StoreContext,
// 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 = defaultProps;
export default ConnectedDraggable;