react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
156 lines (140 loc) • 5.3 kB
Flow
// @flow
import { closest } from '../position';
import isWithin from '../is-within';
import { getCorners } from '../spacing';
import { getVisibleBounds } from '../is-within-visible-bounds-of-droppable';
import type {
Axis,
DroppableDimension,
DroppableDimensionMap,
DroppableId,
Position,
Spacing,
} from '../../types';
type GetBestDroppableArgs = {|
isMovingForward: boolean,
// the current position of the dragging item
pageCenter: Position,
// the home of the draggable
source: DroppableDimension,
// all the droppables in the system
droppables: DroppableDimensionMap,
|}
// This is the shape of an object which we use in this module for
// passing a droppable's clipped bounds along with its dimension
type Candidate = {|
bounds: Spacing,
droppable: DroppableDimension,
|};
export default ({
isMovingForward,
pageCenter,
source,
droppables,
}: GetBestDroppableArgs): ?DroppableDimension => {
const axis: Axis = source.axis;
const sourceBounds: Spacing = getVisibleBounds(source);
const candidates: Candidate[] = Object.keys(droppables)
.map((id: DroppableId): DroppableDimension => droppables[id])
// Remove the source droppable from the list
.filter((droppable: DroppableDimension): boolean => droppable !== source)
// Remove any options that are not enabled
.filter((droppable: DroppableDimension): boolean => droppable.isEnabled)
// Get the true visible bounds of the droppables.
// We calculate it once here and pass it on in an
// object along with the original dimension.
.map((droppable: DroppableDimension): Candidate => ({
bounds: getVisibleBounds(droppable),
droppable,
}))
// Get only droppables that are on the desired side
.filter(({ bounds }: Candidate): boolean => {
if (isMovingForward) {
// is the droppable in front of the source on the cross axis?
return sourceBounds[axis.crossAxisEnd] <=
bounds[axis.crossAxisStart];
}
// is the droppable behind the source on the cross axis?
return bounds[axis.crossAxisEnd] <=
sourceBounds[axis.crossAxisStart];
})
// Must have some overlap on the main axis
.filter(({ bounds }: Candidate): boolean => {
const isBetweenSourceBounds = isWithin(
sourceBounds[axis.start],
sourceBounds[axis.end]
);
const isBetweenDestinationBounds = isWithin(
bounds[axis.start],
bounds[axis.end]
);
return isBetweenSourceBounds(bounds[axis.start]) ||
isBetweenSourceBounds(bounds[axis.end]) ||
isBetweenDestinationBounds(sourceBounds[axis.start]) ||
isBetweenDestinationBounds(sourceBounds[axis.end]);
})
// Filter any droppables which are obscured by their container
.filter(({ droppable }: Candidate) => (
(droppable.page.withoutMargin[axis.crossAxisStart] >=
droppable.container.bounds[axis.crossAxisStart]) &&
(droppable.page.withoutMargin[axis.crossAxisEnd] <=
droppable.container.bounds[axis.crossAxisEnd])
))
// Sort on the cross axis
.sort(({ bounds: a }: Candidate, { bounds: b }: Candidate) => {
const first: number = a[axis.crossAxisStart];
const second: number = b[axis.crossAxisStart];
if (isMovingForward) {
return first - second;
}
return second - first;
})
// Find the droppables that have the same cross axis value as the first item
.filter(({ bounds }: Candidate, index: number, array: Candidate[]): boolean =>
bounds[axis.crossAxisStart] ===
array[0].bounds[axis.crossAxisStart]
);
// no possible candidates
if (!candidates.length) {
return null;
}
// only one result - all done!
if (candidates.length === 1) {
return candidates[0].droppable;
}
// At this point we have a number of candidates that
// all have the same axis.crossAxisStart value.
// Check to see if the center position is within the size of a Droppable on the main axis
const contains: Candidate[] = candidates
.filter(({ bounds }: Candidate) => {
const isWithinDroppable = isWithin(
bounds[axis.start],
bounds[axis.end]
);
return isWithinDroppable(pageCenter[axis.line]);
});
if (contains.length === 1) {
return contains[0].droppable;
}
// The center point of the draggable falls on the boundary between two droppables
if (contains.length > 1) {
// sort on the main axis and choose the first
return contains.sort(({ bounds: a }: Candidate, { bounds: b }: Candidate) => (
a[axis.start] - b[axis.start]
))[0].droppable;
}
// The center is not contained within any droppable
// 1. Find the candidate that has the closest corner
// 2. If there is a tie - choose the one that is first on the main axis
return candidates.sort(({ bounds: a }: Candidate, { bounds: b }: Candidate) => {
const first = closest(pageCenter, getCorners(a));
const second = closest(pageCenter, getCorners(b));
// if the distances are not equal - choose the shortest
if (first !== second) {
return first - second;
}
// They both have the same distance -
// choose the one that is first on the main axis
return a[axis.start] - b[axis.start];
})[0].droppable;
};