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,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);
}
};