react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
183 lines (157 loc) • 5.49 kB
JavaScript
// @flow
import memoizeOne from 'memoize-one';
import { getRect, type Rect, type Position } from 'css-box-model';
import getDraggablesInsideDroppable from './get-draggables-inside-droppable';
import isPositionInFrame from './visibility/is-position-in-frame';
import { patch } from './position';
import { expandByPosition } from './spacing';
import { clip } from './droppable-dimension';
import { toDroppableList } from './dimension-structures';
import type {
Scrollable,
DraggableDimension,
DraggableDimensionMap,
DroppableDimension,
DroppableDimensionMap,
DroppableId,
} from '../types';
const getRequiredGrowth = memoizeOne(
(
draggable: DraggableDimension,
draggables: DraggableDimensionMap,
droppable: DroppableDimension,
): ?Position => {
// We can't always simply add the placeholder size to the droppable size.
// If a droppable has a min-height there will be scenarios where it has
// some items in it, but not enough to completely fill its size.
// In this case - when the droppable already contains excess space - we
// don't need to add the full placeholder size.
const getResult = (existingSpace: number): ?Position => {
// this is the space required for a placeholder
const requiredSpace: number =
draggable.page.marginBox[droppable.axis.size];
if (requiredSpace <= existingSpace) {
return null;
}
const requiredGrowth: Position = patch(
droppable.axis.line,
requiredSpace - existingSpace,
);
return requiredGrowth;
};
const dimensions: DraggableDimension[] = getDraggablesInsideDroppable(
droppable,
draggables,
);
// Droppable is empty
if (!dimensions.length) {
const existingSpace: number =
droppable.page.marginBox[droppable.axis.size];
return getResult(existingSpace);
}
// Droppable has items in it
const endOfDraggables: number =
dimensions[dimensions.length - 1].page.marginBox[droppable.axis.end];
const endOfDroppable: number = droppable.page.marginBox[droppable.axis.end];
const existingSpace: number = endOfDroppable - endOfDraggables;
return getResult(existingSpace);
},
);
type GetBufferedDroppableArgs = {
draggable: DraggableDimension,
draggables: DraggableDimensionMap,
droppable: DroppableDimension,
previousDroppableOverId: ?DroppableId,
};
// TODO: should only expand on the main axis
const getWithGrowth = memoizeOne(
(area: Rect, growth: Position): Rect =>
getRect(expandByPosition(area, growth)),
);
const getClippedRectWithPlaceholder = ({
draggable,
draggables,
droppable,
previousDroppableOverId,
}: GetBufferedDroppableArgs): ?Rect => {
const isHome: boolean =
draggable.descriptor.droppableId === droppable.descriptor.id;
const wasOver: boolean = Boolean(
previousDroppableOverId &&
previousDroppableOverId === droppable.descriptor.id,
);
const clippedPageMarginBox: ?Rect = droppable.viewport.clippedPageMarginBox;
// clipped area is totally hidden behind frame
if (!clippedPageMarginBox) {
return clippedPageMarginBox;
}
// We only include the placeholder size if it's a
// foreign list and is currently being hovered over
if (isHome || !wasOver) {
return clippedPageMarginBox;
}
const requiredGrowth: ?Position = getRequiredGrowth(
draggable,
draggables,
droppable,
);
if (!requiredGrowth) {
return clippedPageMarginBox;
}
const subjectWithGrowth: Rect = getWithGrowth(
clippedPageMarginBox,
requiredGrowth,
);
const closestScrollable: ?Scrollable = droppable.viewport.closestScrollable;
// The droppable has no scroll container
if (!closestScrollable) {
return subjectWithGrowth;
}
// We are not clipping the subject
if (!closestScrollable.shouldClipSubject) {
return subjectWithGrowth;
}
// We need to clip the new subject by the frame which does not change
// This will allow the user to continue to scroll into the placeholder
return clip(closestScrollable.framePageMarginBox, subjectWithGrowth);
};
type Args = {|
target: Position,
draggable: DraggableDimension,
draggables: DraggableDimensionMap,
droppables: DroppableDimensionMap,
previousDroppableOverId: ?DroppableId,
|};
export default ({
target,
draggable,
draggables,
droppables,
previousDroppableOverId,
}: Args): ?DroppableId => {
const maybe: ?DroppableDimension = toDroppableList(droppables)
// only want enabled droppables
.filter((droppable: DroppableDimension) => droppable.isEnabled)
.find(
(droppable: DroppableDimension): boolean => {
// If previously dragging over a droppable we give it a
// bit of room on the subsequent drags so that user and move
// items in the space that the placeholder takes up
const withPlaceholder: ?Rect = getClippedRectWithPlaceholder({
draggable,
draggables,
droppable,
previousDroppableOverId,
});
if (!withPlaceholder) {
return false;
}
// Not checking to see if visible in viewport
// as the target might be off screen if dragging a large draggable
// Not adjusting target for droppable scroll as we are just checking
// if it is over the droppable - not its internal impact
return isPositionInFrame(withPlaceholder)(target);
},
);
return maybe ? maybe.descriptor.id : null;
};