react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
147 lines (125 loc) • 4.76 kB
Flow
// @flow
import memoizeOne from 'memoize-one';
import getArea from './get-area';
import getDraggablesInsideDroppable from './get-draggables-inside-droppable';
import isPositionInFrame from './visibility/is-position-in-frame';
import { patch } from './position';
import { addPosition } from './spacing';
import { clip } from './dimension';
import type {
DraggableDimension,
DraggableDimensionMap,
DroppableDimension,
DroppableDimensionMap,
DroppableId,
Position,
Area,
} 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 dimensions: DraggableDimension[] = getDraggablesInsideDroppable(droppable, draggables);
if (!dimensions.length) {
return null;
}
const endOfDraggables: number =
dimensions[dimensions.length - 1].page.withMargin[droppable.axis.end];
const endOfDroppable: number = droppable.page.withMargin[droppable.axis.end];
const existingSpace: number = endOfDroppable - endOfDraggables;
// this is the space required for a placeholder
const requiredSpace: number = draggable.page.withMargin[droppable.axis.size];
if (requiredSpace <= existingSpace) {
return null;
}
const requiredGrowth: Position = patch(droppable.axis.line, requiredSpace - existingSpace);
return requiredGrowth;
});
type GetBufferedDroppableArgs = {
draggable: DraggableDimension,
draggables: DraggableDimensionMap,
droppable: DroppableDimension,
previousDroppableOverId: ?DroppableId,
};
const getWithGrowth = memoizeOne(
(area: Area, growth: Position): Area => getArea(addPosition(area, growth))
);
const getClippedAreaWithPlaceholder = ({
draggable,
draggables,
droppable,
previousDroppableOverId,
}: GetBufferedDroppableArgs): ?Area => {
const isHome: boolean = draggable.descriptor.droppableId === droppable.descriptor.id;
const isOver: boolean = Boolean(
previousDroppableOverId &&
previousDroppableOverId === droppable.descriptor.id
);
const subject: Area = droppable.viewport.subject;
const frame: Area = droppable.viewport.frame;
const clipped: ?Area = droppable.viewport.clipped;
// clipped area is totally hidden behind frame
if (!clipped) {
return clipped;
}
// We only include the placeholder size if it's a
// foreign list and is currently being hovered over
if (isHome || !isOver) {
return clipped;
}
const requiredGrowth: ?Position = getRequiredGrowth(draggable, draggables, droppable);
if (!requiredGrowth) {
return clipped;
}
const isClippedByFrame: boolean = subject[droppable.axis.size] !== frame[droppable.axis.size];
const subjectWithGrowth = getWithGrowth(subject, requiredGrowth);
if (!isClippedByFrame) {
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(frame, 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 =
Object.keys(droppables)
.map((id: DroppableId): DroppableDimension => droppables[id])
// 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: ?Area = getClippedAreaWithPlaceholder({
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;
};