react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
216 lines (193 loc) • 5.58 kB
Flow
// @flow
import { vertical, horizontal } from './axis';
import getArea from './get-area';
import { add, offset } from './spacing';
import { subtract, negate } from './position';
import type {
DraggableDescriptor,
DroppableDescriptor,
Position,
DraggableDimension,
DroppableDimension,
Direction,
Spacing,
Area,
DroppableDimensionViewport,
} from '../types';
const origin: Position = { x: 0, y: 0 };
export const noSpacing: Spacing = {
top: 0,
right: 0,
bottom: 0,
left: 0,
};
const addPosition = (area: Area, point: Position): Area => {
const { top, right, bottom, left } = area;
return getArea({
top: top + point.y,
left: left + point.x,
bottom: bottom + point.y,
right: right + point.x,
});
};
const addSpacing = (area: Area, spacing: Spacing): Area => {
const { top, right, bottom, left } = area;
return getArea({
// pulling back to increase size
top: top - spacing.top,
left: left - spacing.left,
// pushing forward to increase size
bottom: bottom + spacing.bottom,
right: right + spacing.right,
});
};
type GetDraggableArgs = {|
descriptor: DraggableDescriptor,
client: Area,
margin?: Spacing,
windowScroll?: Position,
|};
export const getDraggableDimension = ({
descriptor,
client,
margin = noSpacing,
windowScroll = origin,
}: GetDraggableArgs): DraggableDimension => {
const withScroll = addPosition(client, windowScroll);
const dimension: DraggableDimension = {
descriptor,
placeholder: {
margin,
withoutMargin: {
width: client.width,
height: client.height,
},
},
// on the viewport
client: {
withoutMargin: getArea(client),
withMargin: getArea(addSpacing(client, margin)),
},
// with scroll
page: {
withoutMargin: getArea(withScroll),
withMargin: getArea(addSpacing(withScroll, margin)),
},
};
return dimension;
};
type GetDroppableArgs = {|
descriptor: DroppableDescriptor,
client: Area,
// optionally provided - and can also be null
frameClient?: ?Area,
frameScroll?: Position,
direction?: Direction,
margin?: Spacing,
padding?: Spacing,
windowScroll?: Position,
// Whether or not the droppable is currently enabled (can change at during a drag)
// defaults to true
isEnabled?: boolean,
|}
// will return null if the subject is completely not visible within frame
export const clip = (frame: Area, subject: Spacing): ?Area => {
const result: Area = getArea({
top: Math.max(subject.top, frame.top),
right: Math.min(subject.right, frame.right),
bottom: Math.min(subject.bottom, frame.bottom),
left: Math.max(subject.left, frame.left),
});
if (result.width < 0 || result.height < 0) {
return null;
}
return result;
};
export const scrollDroppable = (
droppable: DroppableDimension,
newScroll: Position
): DroppableDimension => {
const existing: DroppableDimensionViewport = droppable.viewport;
const scrollDiff: Position = subtract(newScroll, existing.frameScroll.initial);
// a positive scroll difference leads to a negative displacement
// (scrolling down pulls an item upwards)
const scrollDisplacement: Position = negate(scrollDiff);
const displacedSubject: Spacing = offset(existing.subject, scrollDisplacement);
const viewport: DroppableDimensionViewport = {
// does not change
frame: existing.frame,
subject: existing.subject,
// below here changes
frameScroll: {
initial: existing.frameScroll.initial,
current: newScroll,
diff: {
value: scrollDiff,
displacement: scrollDisplacement,
},
},
clipped: clip(existing.frame, displacedSubject),
};
// $ExpectError - using spread
return {
...droppable,
viewport,
};
};
export const getDroppableDimension = ({
descriptor,
client,
frameClient,
frameScroll = origin,
direction = 'vertical',
margin = noSpacing,
padding = noSpacing,
windowScroll = origin,
isEnabled = true,
}: GetDroppableArgs): DroppableDimension => {
const withMargin = addSpacing(client, margin);
const withWindowScroll = addPosition(client, windowScroll);
// If no frameClient is provided, or if the area matches the frameClient, this
// droppable is its own container. In this case we include its margin in the container bounds.
// Otherwise, the container is a scrollable parent. In this case we don't care about margins
// in the container bounds.
const subject: Area = addSpacing(withWindowScroll, margin);
// use client + margin if frameClient is not provided
const frame: Area = (() => {
if (!frameClient) {
return subject;
}
return addPosition(frameClient, windowScroll);
})();
const viewport: DroppableDimensionViewport = {
frame,
frameScroll: {
initial: frameScroll,
// no scrolling yet, so current = initial
current: frameScroll,
diff: {
value: origin,
displacement: origin,
},
},
subject,
clipped: clip(frame, subject),
};
const dimension: DroppableDimension = {
descriptor,
isEnabled,
axis: direction === 'vertical' ? vertical : horizontal,
client: {
withoutMargin: getArea(client),
withMargin: getArea(withMargin),
withMarginAndPadding: getArea(addSpacing(withMargin, padding)),
},
page: {
withoutMargin: getArea(withWindowScroll),
withMargin: subject,
withMarginAndPadding: getArea(addSpacing(withWindowScroll, add(margin, padding))),
},
viewport,
};
return dimension;
};