@hello-pangea/dnd
Version:
Beautiful and accessible drag and drop for lists with React
160 lines (133 loc) • 3.62 kB
text/typescript
import type { Position } from 'css-box-model';
import { add, apply, isEqual, origin } from '../position';
import type { DroppableDimension, Viewport, Scrollable } from '../../types';
interface CanPartiallyScrollArgs {
max: Position;
current: Position;
change: Position;
}
const smallestSigned = apply((value: number) => {
if (value === 0) {
return 0;
}
return value > 0 ? 1 : -1;
});
interface GetRemainderArgs {
current: Position;
max: Position;
change: Position;
}
// We need to figure out how much of the movement
// cannot be done with a scroll
export const getOverlap = (() => {
const getRemainder = (target: number, max: number): number => {
if (target < 0) {
return target;
}
if (target > max) {
return target - max;
}
return 0;
};
return ({ current, max, change }: GetRemainderArgs): Position | null => {
const targetScroll: Position = add(current, change);
const overlap: Position = {
x: getRemainder(targetScroll.x, max.x),
y: getRemainder(targetScroll.y, max.y),
};
if (isEqual(overlap, origin)) {
return null;
}
return overlap;
};
})();
export const canPartiallyScroll = ({
max: rawMax,
current,
change,
}: CanPartiallyScrollArgs): boolean => {
// It is possible for the max scroll to be greater than the current scroll
// when there are scrollbars on the cross axis. We adjust for this by
// increasing the max scroll point if needed
// This will allow movements backwards even if the current scroll is greater than the max scroll
const max: Position = {
x: Math.max(current.x, rawMax.x),
y: Math.max(current.y, rawMax.y),
};
// Only need to be able to move the smallest amount in the desired direction
const smallestChange: Position = smallestSigned(change);
const overlap: Position | null = getOverlap({
max,
current,
change: smallestChange,
});
// no overlap at all - we can move there!
if (!overlap) {
return true;
}
// if there was an x value, but there is no x overlap - then we can scroll on the x!
if (smallestChange.x !== 0 && overlap.x === 0) {
return true;
}
// if there was an y value, but there is no y overlap - then we can scroll on the y!
if (smallestChange.y !== 0 && overlap.y === 0) {
return true;
}
return false;
};
export const canScrollWindow = (
viewport: Viewport,
change: Position,
): boolean =>
canPartiallyScroll({
current: viewport.scroll.current,
max: viewport.scroll.max,
change,
});
export const getWindowOverlap = (
viewport: Viewport,
change: Position,
): Position | null => {
if (!canScrollWindow(viewport, change)) {
return null;
}
const max: Position = viewport.scroll.max;
const current: Position = viewport.scroll.current;
return getOverlap({
current,
max,
change,
});
};
export const canScrollDroppable = (
droppable: DroppableDimension,
change: Position,
): boolean => {
const frame: Scrollable | null = droppable.frame;
// Cannot scroll when there is no scrollable
if (!frame) {
return false;
}
return canPartiallyScroll({
current: frame.scroll.current,
max: frame.scroll.max,
change,
});
};
export const getDroppableOverlap = (
droppable: DroppableDimension,
change: Position,
): Position | null => {
const frame: Scrollable | null = droppable.frame;
if (!frame) {
return null;
}
if (!canScrollDroppable(droppable, change)) {
return null;
}
return getOverlap({
current: frame.scroll.current,
max: frame.scroll.max,
change,
});
};