react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
162 lines (140 loc) • 5.53 kB
Flow
// @flow
import { closest } from '../position';
import isWithin from '../is-within';
import { getCorners } from '../spacing';
import getViewport from '../visibility/get-viewport';
import isVisibleThroughFrame from '../visibility/is-visible-through-frame';
import type {
Axis,
DroppableDimension,
DroppableDimensionMap,
DroppableId,
Position,
Area,
} 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,
|}
const getSafeClipped = (droppable: DroppableDimension): Area => {
const area: ?Area = droppable.viewport.clipped;
if (!area) {
throw new Error('cannot get clipped area from droppable');
}
return area;
};
export default ({
isMovingForward,
pageCenter,
source,
droppables,
}: GetBestDroppableArgs): ?DroppableDimension => {
const sourceClipped: ?Area = source.viewport.clipped;
if (!sourceClipped) {
return null;
}
const axis: Axis = source.axis;
const isBetweenSourceClipped = isWithin(
sourceClipped[axis.start],
sourceClipped[axis.end]
);
const viewport: Area = getViewport();
// const candidates: Candidate[] = Object.keys(droppables)
const candidates: DroppableDimension[] = 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)
// Remove any droppables that have invisible subjects
.filter((droppable: DroppableDimension): boolean => Boolean(droppable.viewport.clipped))
// Remove any droppables that are not partially visible
.filter((droppable: DroppableDimension): boolean => (
isVisibleThroughFrame(viewport)(droppable.viewport.frame)
))
.filter((droppable: DroppableDimension): boolean => {
const targetClipped: Area = getSafeClipped(droppable);
if (isMovingForward) {
// is the droppable in front of the source on the cross axis?
return sourceClipped[axis.crossAxisEnd] <=
targetClipped[axis.crossAxisStart];
}
// is the droppable behind the source on the cross axis?
return targetClipped[axis.crossAxisEnd] <=
sourceClipped[axis.crossAxisStart];
})
// Must have some overlap on the main axis
.filter((droppable: DroppableDimension): boolean => {
const targetClipped: Area = getSafeClipped(droppable);
const isBetweenDestinationClipped = isWithin(
targetClipped[axis.start],
targetClipped[axis.end]
);
return isBetweenSourceClipped(targetClipped[axis.start]) ||
isBetweenSourceClipped(targetClipped[axis.end]) ||
isBetweenDestinationClipped(sourceClipped[axis.start]) ||
isBetweenDestinationClipped(sourceClipped[axis.end]);
})
// Sort on the cross axis
.sort((a: DroppableDimension, b: DroppableDimension) => {
const first: number = getSafeClipped(a)[axis.crossAxisStart];
const second: number = getSafeClipped(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((droppable: DroppableDimension, index: number, array: DroppableDimension[]): boolean =>
getSafeClipped(droppable)[axis.crossAxisStart] ===
getSafeClipped(array[0])[axis.crossAxisStart]
);
// no possible candidates
if (!candidates.length) {
return null;
}
// only one result - all done!
if (candidates.length === 1) {
return candidates[0];
}
// 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: DroppableDimension[] = candidates
.filter((droppable: DroppableDimension) => {
const isWithinDroppable = isWithin(
getSafeClipped(droppable)[axis.start],
getSafeClipped(droppable)[axis.end]
);
return isWithinDroppable(pageCenter[axis.line]);
});
if (contains.length === 1) {
return contains[0];
}
// 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((a: DroppableDimension, b: DroppableDimension): number => (
getSafeClipped(a)[axis.start] - getSafeClipped(b)[axis.start]
))[0];
}
// 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((a: DroppableDimension, b: DroppableDimension): number => {
const first = closest(pageCenter, getCorners(getSafeClipped(a)));
const second = closest(pageCenter, getCorners(getSafeClipped(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 getSafeClipped(a)[axis.start] - getSafeClipped(b)[axis.start];
})[0];
};