react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
246 lines (213 loc) • 6.26 kB
JavaScript
// @flow
import { vertical, horizontal } from './axis';
import getArea from './get-area';
import { offsetByPosition, expandBySpacing, shrinkBySpacing } from './spacing';
import { subtract, negate } from './position';
import getMaxScroll from './get-max-scroll';
import type {
DraggableDescriptor,
DroppableDescriptor,
Position,
DraggableDimension,
DroppableDimension,
Direction,
Spacing,
Area,
DroppableDimensionViewport,
ClosestScrollable,
BoxModel,
} from '../types';
const origin: Position = { x: 0, y: 0 };
export const noSpacing: Spacing = {
top: 0,
right: 0,
bottom: 0,
left: 0,
};
type GetDraggableArgs = {|
descriptor: DraggableDescriptor,
borderBox: Area,
tagName?: string,
display?: string,
margin?: Spacing,
windowScroll?: Position,
|};
export const getDraggableDimension = ({
descriptor,
borderBox,
tagName = 'div',
display = 'block',
margin = noSpacing,
windowScroll = origin,
}: GetDraggableArgs): DraggableDimension => {
const pageBorderBox: Spacing = offsetByPosition(borderBox, windowScroll);
const dimension: DraggableDimension = {
descriptor,
placeholder: {
borderBox,
margin,
tagName,
display,
},
// on the viewport
client: {
borderBox,
marginBox: getArea(expandBySpacing(borderBox, margin)),
},
// with scroll
page: {
borderBox: getArea(pageBorderBox),
marginBox: getArea(expandBySpacing(pageBorderBox, margin)),
},
};
return dimension;
};
// 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 => {
if (!droppable.viewport.closestScrollable) {
console.error('Cannot scroll droppble that does not have a closest scrollable');
return droppable;
}
const existingScrollable: ClosestScrollable = droppable.viewport.closestScrollable;
const frame: Area = existingScrollable.frame;
const scrollDiff: Position = subtract(newScroll, existingScrollable.scroll.initial);
// a positive scroll difference leads to a negative displacement
// (scrolling down pulls an item upwards)
const scrollDisplacement: Position = negate(scrollDiff);
// Sometimes it is possible to scroll beyond the max point.
// This can occur when scrolling a foreign list that now has a placeholder.
const closestScrollable: ClosestScrollable = {
frame: existingScrollable.frame,
shouldClipSubject: existingScrollable.shouldClipSubject,
scroll: {
initial: existingScrollable.scroll.initial,
current: newScroll,
diff: {
value: scrollDiff,
displacement: scrollDisplacement,
},
// TODO: rename 'softMax?'
max: existingScrollable.scroll.max,
},
};
const displacedSubject: Spacing =
offsetByPosition(droppable.viewport.subject, scrollDisplacement);
const clipped: ?Area = closestScrollable.shouldClipSubject ?
clip(frame, displacedSubject) :
getArea(displacedSubject);
const viewport: DroppableDimensionViewport = {
closestScrollable,
subject: droppable.viewport.subject,
clipped,
};
const result: DroppableDimension = {
...droppable,
viewport,
};
return result;
};
type GetDroppableArgs = {|
descriptor: DroppableDescriptor,
borderBox: Area,
// optionally provided - and can also be null
closest?: ?{|
frameBorderBox: Area,
scrollWidth: number,
scrollHeight: number,
scroll: Position,
shouldClipSubject: boolean,
|},
direction?: Direction,
margin?: Spacing,
padding ?: Spacing,
border?: Spacing,
windowScroll?: Position,
// Whether or not the droppable is currently enabled (can change at during a drag)
// defaults to true
isEnabled?: boolean,
|}
export const getDroppableDimension = ({
descriptor,
borderBox,
closest,
direction = 'vertical',
margin = noSpacing,
padding = noSpacing,
border = noSpacing,
windowScroll = origin,
isEnabled = true,
}: GetDroppableArgs): DroppableDimension => {
const marginBox: Area = getArea(expandBySpacing(borderBox, margin));
const paddingBox: Area = getArea(shrinkBySpacing(borderBox, border));
const contentBox: Area = getArea(shrinkBySpacing(paddingBox, padding));
const client: BoxModel = {
borderBox,
marginBox,
contentBox,
};
const page: BoxModel = {
marginBox: getArea(offsetByPosition(marginBox, windowScroll)),
borderBox: getArea(offsetByPosition(borderBox, windowScroll)),
contentBox: getArea(offsetByPosition(contentBox, windowScroll)),
};
const subject: Area = page.marginBox;
const closestScrollable: ?ClosestScrollable = (() => {
if (!closest) {
return null;
}
const frame: Area = getArea(offsetByPosition(closest.frameBorderBox, windowScroll));
const maxScroll: Position = getMaxScroll({
scrollHeight: closest.scrollHeight,
scrollWidth: closest.scrollWidth,
height: frame.height,
width: frame.width,
});
const result: ClosestScrollable = {
frame,
shouldClipSubject: closest.shouldClipSubject,
scroll: {
initial: closest.scroll,
// no scrolling yet, so current = initial
current: closest.scroll,
max: maxScroll,
diff: {
value: origin,
displacement: origin,
},
},
};
return result;
})();
const clipped: ?Area = (closestScrollable && closestScrollable.shouldClipSubject) ?
clip(closestScrollable.frame, subject) :
subject;
const viewport: DroppableDimensionViewport = {
closestScrollable,
subject,
clipped,
};
const dimension: DroppableDimension = {
descriptor,
isEnabled,
axis: direction === 'vertical' ? vertical : horizontal,
client,
page,
viewport,
};
return dimension;
};