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