first commit

This commit is contained in:
2026-06-24 09:48:54 +02:00
commit 41e62ddcad
33739 changed files with 4266226 additions and 0 deletions
@@ -0,0 +1,7 @@
import type { State, DraggingState } from '../../types';
export interface AutoScroller {
start: (state: DraggingState) => void;
stop: () => void;
scroll: (state: State) => void;
}
+159
View File
@@ -0,0 +1,159 @@
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,
});
};
@@ -0,0 +1,50 @@
import { RecursivePartial } from '../../../lib-types';
/**
* Customize autoScroller behavior
*/
export interface AutoScrollerOptions {
/**
* Percentage distance from edge of container at which to start auto scrolling.
* ex. 0.1 or 0.9
*/
startFromPercentage: number;
/**
* Percentage distance from edge of container at which max scroll speed is achieved.
* Should be less than startFromPercentage
*/
maxScrollAtPercentage: number;
/**
* Maximum pixels to scroll per frame
*/
maxPixelScroll: number;
/**
* A function used to ease a percentage value
* A simple linear function would be: (percentage) => percentage;
* percentage is between 0 and 1
* result must be between 0 and 1
*/
ease: (percentage: number) => number;
durationDampening: {
/**
* How long to dampen the speed of an auto scroll from the start of a drag in milliseconds
*/
stopDampeningAt: number;
/**
* When to start accelerating the reduction of duration dampening in milliseconds
*/
accelerateAt: number;
};
/**
* Whether or not autoscroll should be turned off entirely
*/
disabled: boolean;
}
export type PartialAutoScrollerOptions = RecursivePartial<AutoScrollerOptions>;
@@ -0,0 +1,14 @@
import { AutoScrollerOptions } from './auto-scroller-options-types';
// default autoScroll configuration options
export const defaultAutoScrollerOptions: AutoScrollerOptions = {
startFromPercentage: 0.25,
maxScrollAtPercentage: 0.05,
maxPixelScroll: 28,
ease: (percentage: number): number => percentage ** 2,
durationDampening: {
stopDampeningAt: 1200,
accelerateAt: 360,
},
disabled: false,
};
@@ -0,0 +1,76 @@
import memoizeOne from 'memoize-one';
import type { Position } from 'css-box-model';
import type {
DroppableDimension,
DroppableDimensionMap,
DroppableId,
} from '../../../types';
import { invariant } from '../../../invariant';
import isPositionInFrame from '../../visibility/is-position-in-frame';
import { toDroppableList } from '../../dimension-structures';
const getScrollableDroppables = memoizeOne(
(droppables: DroppableDimensionMap): DroppableDimension[] =>
toDroppableList(droppables).filter(
(droppable: DroppableDimension): boolean => {
// exclude disabled droppables
if (!droppable.isEnabled) {
return false;
}
// only want droppables that are scrollable
if (!droppable.frame) {
return false;
}
return true;
},
),
);
const getScrollableDroppableOver = (
target: Position,
droppables: DroppableDimensionMap,
): DroppableDimension | null => {
const maybe =
getScrollableDroppables(droppables).find(
(droppable: DroppableDimension): boolean => {
invariant(droppable.frame, 'Invalid result');
return isPositionInFrame(droppable.frame.pageMarginBox)(target);
},
) || null;
return maybe;
};
interface Api {
center: Position;
destination: DroppableId | null;
droppables: DroppableDimensionMap;
}
export default ({
center,
destination,
droppables,
}: Api): DroppableDimension | null => {
// We need to scroll the best droppable frame we can so that the
// placeholder buffer logic works correctly
if (destination) {
const dimension: DroppableDimension = droppables[destination];
if (!dimension.frame) {
return null;
}
return dimension;
}
// 2. If we are not over a droppable - are we over a droppable frame?
const dimension: DroppableDimension | null = getScrollableDroppableOver(
center,
droppables,
);
return dimension;
};
@@ -0,0 +1,42 @@
import type { Position, Rect } from 'css-box-model';
import type { Scrollable, DroppableDimension } from '../../../types';
import getScroll from './get-scroll';
import { canScrollDroppable } from '../can-scroll';
import { AutoScrollerOptions } from './auto-scroller-options-types';
interface Args {
droppable: DroppableDimension;
subject: Rect;
center: Position;
dragStartTime: number;
shouldUseTimeDampening: boolean;
getAutoScrollerOptions: () => AutoScrollerOptions;
}
export default ({
droppable,
subject,
center,
dragStartTime,
shouldUseTimeDampening,
getAutoScrollerOptions,
}: Args): Position | null => {
// We know this has a closestScrollable
const frame: Scrollable | null = droppable.frame;
// this should never happen - just being safe
if (!frame) {
return null;
}
const scroll: Position | null = getScroll({
dragStartTime,
container: frame.pageMarginBox,
subject,
center,
shouldUseTimeDampening,
getAutoScrollerOptions,
});
return scroll && canScrollDroppable(droppable, scroll) ? scroll : null;
};
@@ -0,0 +1,24 @@
import { warning } from '../../../dev-warning';
interface Args {
startOfRange: number;
endOfRange: number;
current: number;
}
export default ({ startOfRange, endOfRange, current }: Args): number => {
const range: number = endOfRange - startOfRange;
if (range === 0) {
warning(`
Detected distance range of 0 in the fluid auto scroller
This is unexpected and would cause a divide by 0 issue.
Not allowing an auto scroll
`);
return 0;
}
const currentInRange: number = current - startOfRange;
const percentage: number = currentInRange / range;
return percentage;
};
@@ -0,0 +1,33 @@
import type { Rect, Position } from 'css-box-model';
interface Args {
container: Rect;
subject: Rect;
proposedScroll: Position;
}
export default ({
container,
subject,
proposedScroll,
}: Args): Position | null => {
const isTooBigVertically: boolean = subject.height > container.height;
const isTooBigHorizontally: boolean = subject.width > container.width;
// not too big on any axis
if (!isTooBigHorizontally && !isTooBigVertically) {
return proposedScroll;
}
// too big on both axis
if (isTooBigHorizontally && isTooBigVertically) {
return null;
}
// Only too big on one axis
// Exclude the axis that we cannot scroll on
return {
x: isTooBigHorizontally ? 0 : proposedScroll.x,
y: isTooBigVertically ? 0 : proposedScroll.y,
};
};
@@ -0,0 +1,44 @@
import getPercentage from '../../get-percentage';
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
import minScroll from './min-scroll';
export default (
proposedScroll: number,
dragStartTime: number,
getAutoScrollerOptions: () => AutoScrollerOptions,
): number => {
const autoScrollerOptions = getAutoScrollerOptions();
const accelerateAt: number =
autoScrollerOptions.durationDampening.accelerateAt;
const stopAt: number = autoScrollerOptions.durationDampening.stopDampeningAt;
const startOfRange: number = dragStartTime;
const endOfRange: number = stopAt;
const now: number = Date.now();
const runTime: number = now - startOfRange;
// we have finished the time dampening period
if (runTime >= stopAt) {
return proposedScroll;
}
// Up to this point we know there is a proposed scroll
// but we have not reached our accelerate point
// Return the minimum amount of scroll
if (runTime < accelerateAt) {
return minScroll;
}
const betweenAccelerateAtAndStopAtPercentage: number = getPercentage({
startOfRange: accelerateAt,
endOfRange,
current: runTime,
});
const scroll: number =
proposedScroll *
autoScrollerOptions.ease(betweenAccelerateAtAndStopAtPercentage);
return Math.ceil(scroll);
};
@@ -0,0 +1,32 @@
import type { Rect } from 'css-box-model';
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
import type { Axis } from '../../../../../types';
import { defaultAutoScrollerOptions } from '../../config';
// all in pixels
export interface DistanceThresholds {
startScrollingFrom: number;
maxScrollValueAt: number;
}
// converts the percentages in the config into actual pixel values
export default (
container: Rect,
axis: Axis,
getAutoScrollerOptions: () => AutoScrollerOptions = () =>
defaultAutoScrollerOptions,
): DistanceThresholds => {
const autoScrollerOptions = getAutoScrollerOptions();
const startScrollingFrom: number =
container[axis.size] * autoScrollerOptions.startFromPercentage;
const maxScrollValueAt: number =
container[axis.size] * autoScrollerOptions.maxScrollAtPercentage;
const thresholds: DistanceThresholds = {
startScrollingFrom,
maxScrollValueAt,
};
return thresholds;
};
@@ -0,0 +1,66 @@
import type { DistanceThresholds } from './get-distance-thresholds';
import getPercentage from '../../get-percentage';
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
import minScroll from './min-scroll';
import { defaultAutoScrollerOptions } from '../../config';
export default (
distanceToEdge: number,
thresholds: DistanceThresholds,
getAutoScrollerOptions: () => AutoScrollerOptions = () =>
defaultAutoScrollerOptions,
): number => {
const autoScrollerOptions = getAutoScrollerOptions();
/*
// This function only looks at the distance to one edge
// Example: looking at bottom edge
|----------------------------------|
| |
| |
| |
| |
| | => no scroll in this range
| |
| |
| startScrollingFrom (eg 100px) |
| |
| | => increased scroll value the closer to maxScrollValueAt
| maxScrollValueAt (eg 10px) |
| | => max scroll value in this range
|----------------------------------|
*/
// too far away to auto scroll
if (distanceToEdge > thresholds.startScrollingFrom) {
return 0;
}
// use max speed when on or over boundary
if (distanceToEdge <= thresholds.maxScrollValueAt) {
return autoScrollerOptions.maxPixelScroll;
}
// when just going on the boundary return the minimum integer
if (distanceToEdge === thresholds.startScrollingFrom) {
return minScroll;
}
// to get the % past startScrollingFrom we will calculate
// the % the value is from maxScrollValueAt and then invert it
const percentageFromMaxScrollValueAt: number = getPercentage({
startOfRange: thresholds.maxScrollValueAt,
endOfRange: thresholds.startScrollingFrom,
current: distanceToEdge,
});
const percentageFromStartScrollingFrom: number =
1 - percentageFromMaxScrollValueAt;
const scroll: number =
autoScrollerOptions.maxPixelScroll *
autoScrollerOptions.ease(percentageFromStartScrollingFrom);
// scroll will always be a positive integer
return Math.ceil(scroll);
};
@@ -0,0 +1,48 @@
import type { DistanceThresholds } from './get-distance-thresholds';
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
import getValueFromDistance from './get-value-from-distance';
import dampenValueByTime from './dampen-value-by-time';
import minScroll from './min-scroll';
interface Args {
distanceToEdge: number;
thresholds: DistanceThresholds;
dragStartTime: number;
shouldUseTimeDampening: boolean;
getAutoScrollerOptions: () => AutoScrollerOptions;
}
export default ({
distanceToEdge,
thresholds,
dragStartTime,
shouldUseTimeDampening,
getAutoScrollerOptions,
}: Args): number => {
const scroll: number = getValueFromDistance(
distanceToEdge,
thresholds,
getAutoScrollerOptions,
);
// not enough distance to trigger a minimum scroll
// we can bail here
if (scroll === 0) {
return 0;
}
// Dampen an auto scroll speed based on duration of drag
if (!shouldUseTimeDampening) {
return scroll;
}
// Once we know an auto scroll should occur based on distance,
// we must let at least 1px through to trigger a scroll event an
// another auto scroll call
return Math.max(
dampenValueByTime(scroll, dragStartTime, getAutoScrollerOptions),
minScroll,
);
};
@@ -0,0 +1,53 @@
import type { Rect, Spacing } from 'css-box-model';
import getDistanceThresholds from './get-distance-thresholds';
import { AutoScrollerOptions } from '../../auto-scroller-options-types';
import type { DistanceThresholds } from './get-distance-thresholds';
import type { Axis } from '../../../../../types';
import getValue from './get-value';
interface GetOnAxisArgs {
container: Rect;
distanceToEdges: Spacing;
dragStartTime: number;
axis: Axis;
shouldUseTimeDampening: boolean;
getAutoScrollerOptions: () => AutoScrollerOptions;
}
export default ({
container,
distanceToEdges,
dragStartTime,
axis,
shouldUseTimeDampening,
getAutoScrollerOptions,
}: GetOnAxisArgs): number => {
const thresholds: DistanceThresholds = getDistanceThresholds(
container,
axis,
getAutoScrollerOptions,
);
const isCloserToEnd: boolean =
distanceToEdges[axis.end] < distanceToEdges[axis.start];
if (isCloserToEnd) {
return getValue({
distanceToEdge: distanceToEdges[axis.end],
thresholds,
dragStartTime,
shouldUseTimeDampening,
getAutoScrollerOptions,
});
}
return (
-1 *
getValue({
distanceToEdge: distanceToEdges[axis.start],
thresholds,
dragStartTime,
shouldUseTimeDampening,
getAutoScrollerOptions,
})
);
};
@@ -0,0 +1,2 @@
// A scroll event will only be triggered when there is a value of at least 1px change
export default 1;
@@ -0,0 +1,80 @@
import type { Position, Rect, Spacing } from 'css-box-model';
import { apply, isEqual, origin } from '../../../position';
import getScrollOnAxis from './get-scroll-on-axis';
import adjustForSizeLimits from './adjust-for-size-limits';
import { horizontal, vertical } from '../../../axis';
import { AutoScrollerOptions } from '../auto-scroller-options-types';
// will replace -0 and replace with +0
const clean = apply((value: number) => (value === 0 ? 0 : value));
interface Args {
dragStartTime: number;
container: Rect;
subject: Rect;
center: Position;
shouldUseTimeDampening: boolean;
getAutoScrollerOptions: () => AutoScrollerOptions;
}
export default ({
dragStartTime,
container,
subject,
center,
shouldUseTimeDampening,
getAutoScrollerOptions,
}: Args): Position | null => {
// get distance to each edge
const distanceToEdges: Spacing = {
top: center.y - container.top,
right: container.right - center.x,
bottom: container.bottom - center.y,
left: center.x - container.left,
};
// 1. Figure out which x,y values are the best target
// 2. Can the container scroll in that direction at all?
// If no for both directions, then return null
// 3. Is the center close enough to a edge to start a drag?
// 4. Based on the distance, calculate the speed at which a scroll should occur
// The lower distance value the faster the scroll should be.
// Maximum speed value should be hit before the distance is 0
// Negative values to not continue to increase the speed
const y: number = getScrollOnAxis({
container,
distanceToEdges,
dragStartTime,
axis: vertical,
shouldUseTimeDampening,
getAutoScrollerOptions,
});
const x: number = getScrollOnAxis({
container,
distanceToEdges,
dragStartTime,
axis: horizontal,
shouldUseTimeDampening,
getAutoScrollerOptions,
});
const required: Position = clean({ x, y });
// nothing required
if (isEqual(required, origin)) {
return null;
}
// need to not scroll in a direction that we are too big to scroll in
const limited: Position | null = adjustForSizeLimits({
container,
subject,
proposedScroll: required,
});
if (!limited) {
return null;
}
return isEqual(limited, origin) ? null : limited;
};
@@ -0,0 +1,34 @@
import type { Position, Rect } from 'css-box-model';
import type { Viewport } from '../../../types';
import getScroll from './get-scroll';
import { canScrollWindow } from '../can-scroll';
import { AutoScrollerOptions } from './auto-scroller-options-types';
interface Args {
viewport: Viewport;
subject: Rect;
center: Position;
dragStartTime: number;
shouldUseTimeDampening: boolean;
getAutoScrollerOptions: () => AutoScrollerOptions;
}
export default ({
viewport,
subject,
center,
dragStartTime,
shouldUseTimeDampening,
getAutoScrollerOptions,
}: Args): Position | null => {
const scroll: Position | null = getScroll({
dragStartTime,
container: viewport.frame,
subject,
center,
shouldUseTimeDampening,
getAutoScrollerOptions,
});
return scroll && canScrollWindow(viewport, scroll) ? scroll : null;
};
@@ -0,0 +1,95 @@
import rafSchd from 'raf-schd';
import type { Position } from 'css-box-model';
import type { DraggingState, DroppableId } from '../../../types';
import scroll from './scroll';
import { invariant } from '../../../invariant';
import * as timings from '../../../debug/timings';
import { AutoScrollerOptions } from './auto-scroller-options-types';
import { defaultAutoScrollerOptions } from './config';
export interface PublicArgs {
scrollWindow: (change: Position) => void;
scrollDroppable: (id: DroppableId, change: Position) => void;
getAutoScrollerOptions?: () => AutoScrollerOptions;
}
export interface FluidScroller {
scroll: (state: DraggingState) => void;
start: (state: DraggingState) => void;
stop: () => void;
}
interface WhileDragging {
dragStartTime: number;
shouldUseTimeDampening: boolean;
}
export default ({
scrollWindow,
scrollDroppable,
getAutoScrollerOptions = () => defaultAutoScrollerOptions,
}: PublicArgs): FluidScroller => {
const scheduleWindowScroll = rafSchd(scrollWindow);
const scheduleDroppableScroll = rafSchd(scrollDroppable);
let dragging: WhileDragging | null = null;
const tryScroll = (state: DraggingState): void => {
invariant(dragging, 'Cannot fluid scroll if not dragging');
const { shouldUseTimeDampening, dragStartTime } = dragging;
scroll({
state,
scrollWindow: scheduleWindowScroll,
scrollDroppable: scheduleDroppableScroll,
dragStartTime,
shouldUseTimeDampening,
getAutoScrollerOptions,
});
};
const start = (state: DraggingState) => {
timings.start('starting fluid scroller');
invariant(!dragging, 'Cannot start auto scrolling when already started');
const dragStartTime: number = Date.now();
let wasScrollNeeded = false;
const fakeScrollCallback = () => {
wasScrollNeeded = true;
};
scroll({
state,
dragStartTime: 0,
shouldUseTimeDampening: false,
scrollWindow: fakeScrollCallback,
scrollDroppable: fakeScrollCallback,
getAutoScrollerOptions,
});
dragging = {
dragStartTime,
shouldUseTimeDampening: wasScrollNeeded,
};
timings.finish('starting fluid scroller');
// we know an auto scroll is needed - let's do it!
if (wasScrollNeeded) {
tryScroll(state);
}
};
const stop = () => {
// can be called defensively
if (!dragging) {
return;
}
scheduleWindowScroll.cancel();
scheduleDroppableScroll.cancel();
dragging = null;
};
return {
start,
stop,
scroll: tryScroll,
};
};
@@ -0,0 +1,76 @@
import type { Position, Rect } from 'css-box-model';
import type {
DraggingState,
DroppableId,
DraggableDimension,
DroppableDimension,
Viewport,
} from '../../../types';
import getBestScrollableDroppable from './get-best-scrollable-droppable';
import whatIsDraggedOver from '../../droppable/what-is-dragged-over';
import getWindowScrollChange from './get-window-scroll-change';
import getDroppableScrollChange from './get-droppable-scroll-change';
import { AutoScrollerOptions } from './auto-scroller-options-types';
interface Args {
state: DraggingState;
dragStartTime: number;
shouldUseTimeDampening: boolean;
scrollWindow: (scroll: Position) => void;
scrollDroppable: (id: DroppableId, scroll: Position) => void;
getAutoScrollerOptions: () => AutoScrollerOptions;
}
export default ({
state,
dragStartTime,
shouldUseTimeDampening,
scrollWindow,
scrollDroppable,
getAutoScrollerOptions,
}: Args): void => {
const center: Position = state.current.page.borderBoxCenter;
const draggable: DraggableDimension =
state.dimensions.draggables[state.critical.draggable.id];
const subject: Rect = draggable.page.marginBox;
// 1. Can we scroll the viewport?
if (state.isWindowScrollAllowed) {
const viewport: Viewport = state.viewport;
const change: Position | null = getWindowScrollChange({
dragStartTime,
viewport,
subject,
center,
shouldUseTimeDampening,
getAutoScrollerOptions,
});
if (change) {
scrollWindow(change);
return;
}
}
const droppable: DroppableDimension | null = getBestScrollableDroppable({
center,
destination: whatIsDraggedOver(state.impact),
droppables: state.dimensions.droppables,
});
if (!droppable) {
return;
}
const change: Position | null = getDroppableScrollChange({
dragStartTime,
droppable,
subject,
center,
shouldUseTimeDampening,
getAutoScrollerOptions,
});
if (change) {
scrollDroppable(droppable.descriptor.id, change);
}
};
+64
View File
@@ -0,0 +1,64 @@
import type { Position } from 'css-box-model';
import createFluidScroller from './fluid-scroller';
import type { FluidScroller } from './fluid-scroller';
import createJumpScroller from './jump-scroller';
import type { JumpScroller } from './jump-scroller';
import type { AutoScroller } from './auto-scroller-types';
import type { DroppableId, State } from '../../types';
import type { MoveArgs } from '../action-creators';
import { AutoScrollerOptions } from './fluid-scroller/auto-scroller-options-types';
export interface Args {
scrollWindow: (offset: Position) => void;
scrollDroppable: (id: DroppableId, change: Position) => void;
move: (args: MoveArgs) => unknown;
getAutoScrollerOptions: () => AutoScrollerOptions;
}
export default ({
scrollDroppable,
scrollWindow,
move,
getAutoScrollerOptions,
}: Args): AutoScroller => {
const fluidScroller: FluidScroller = createFluidScroller({
scrollWindow,
scrollDroppable,
getAutoScrollerOptions,
});
const jumpScroll: JumpScroller = createJumpScroller({
move,
scrollWindow,
scrollDroppable,
});
const scroll = (state: State) => {
const autoScrollerOptions = getAutoScrollerOptions();
// Only allowing auto scrolling in the DRAGGING phase
// and when autoScroll is not disabled
if (autoScrollerOptions.disabled || state.phase !== 'DRAGGING') {
return;
}
if (state.movementMode === 'FLUID') {
fluidScroller.scroll(state);
return;
}
if (!state.scrollJumpRequest) {
return;
}
jumpScroll(state);
};
const scroller: AutoScroller = {
scroll,
start: fluidScroller.start,
stop: fluidScroller.stop,
};
return scroller;
};
+138
View File
@@ -0,0 +1,138 @@
import type { Position } from 'css-box-model';
import { invariant } from '../../invariant';
import { add, subtract } from '../position';
import {
canScrollWindow,
canScrollDroppable,
getWindowOverlap,
getDroppableOverlap,
} from './can-scroll';
import whatIsDraggedOver from '../droppable/what-is-dragged-over';
import type { MoveArgs } from '../action-creators';
import type {
DroppableDimension,
Viewport,
DraggingState,
DroppableId,
} from '../../types';
interface Args {
scrollDroppable: (id: DroppableId, change: Position) => void;
scrollWindow: (offset: Position) => void;
move: (args: MoveArgs) => unknown;
}
export type JumpScroller = (state: DraggingState) => void;
type Remainder = Position;
export default ({
move,
scrollDroppable,
scrollWindow,
}: Args): JumpScroller => {
const moveByOffset = (state: DraggingState, offset: Position) => {
const client: Position = add(state.current.client.selection, offset);
move({ client });
};
const scrollDroppableAsMuchAsItCan = (
droppable: DroppableDimension,
change: Position,
): Remainder | null => {
// Droppable cannot absorb any of the scroll
if (!canScrollDroppable(droppable, change)) {
return change;
}
const overlap: Position | null = getDroppableOverlap(droppable, change);
// Droppable can absorb the entire change
if (!overlap) {
scrollDroppable(droppable.descriptor.id, change);
return null;
}
// Droppable can only absorb a part of the change
const whatTheDroppableCanScroll: Position = subtract(change, overlap);
scrollDroppable(droppable.descriptor.id, whatTheDroppableCanScroll);
const remainder: Position = subtract(change, whatTheDroppableCanScroll);
return remainder;
};
const scrollWindowAsMuchAsItCan = (
isWindowScrollAllowed: boolean,
viewport: Viewport,
change: Position,
): Position | null => {
if (!isWindowScrollAllowed) {
return change;
}
if (!canScrollWindow(viewport, change)) {
// window cannot absorb any of the scroll
return change;
}
const overlap: Position | null = getWindowOverlap(viewport, change);
// window can absorb entire scroll
if (!overlap) {
scrollWindow(change);
return null;
}
// window can only absorb a part of the scroll
const whatTheWindowCanScroll: Position = subtract(change, overlap);
scrollWindow(whatTheWindowCanScroll);
const remainder: Position = subtract(change, whatTheWindowCanScroll);
return remainder;
};
const jumpScroller: JumpScroller = (state: DraggingState) => {
const request: Position | null = state.scrollJumpRequest;
if (!request) {
return;
}
const destination: DroppableId | null = whatIsDraggedOver(state.impact);
invariant(
destination,
'Cannot perform a jump scroll when there is no destination',
);
// 1. We scroll the droppable first if we can to avoid the draggable
// leaving the list
const droppableRemainder: Position | null = scrollDroppableAsMuchAsItCan(
state.dimensions.droppables[destination],
request,
);
// droppable absorbed the entire scroll
if (!droppableRemainder) {
return;
}
const viewport: Viewport = state.viewport;
const windowRemainder: Position | null = scrollWindowAsMuchAsItCan(
state.isWindowScrollAllowed,
viewport,
droppableRemainder,
);
// window could absorb all the droppable remainder
if (!windowRemainder) {
return;
}
// The entire scroll could not be absorbed by the droppable and window
// so we manually move whatever is left
moveByOffset(state, windowRemainder);
};
return jumpScroller;
};