react-beautiful-dnd-next
Version:
Beautiful and accessible drag and drop for lists with React
397 lines (342 loc) • 10.2 kB
JavaScript
// @flow
import type { BoxModel, Rect, Position } from 'css-box-model';
export type Id = string;
export type DraggableId = Id;
export type DroppableId = Id;
export type TypeId = Id;
export type DroppableDescriptor = {|
id: DroppableId,
type: TypeId,
|};
export type DraggableDescriptor = {|
id: DraggableId,
index: number,
// Inherited from Droppable
droppableId: DroppableId,
// This is technically redundant but it avoids
// needing to look up a parent droppable just to get its type
type: TypeId,
|};
export type Direction = 'horizontal' | 'vertical';
export type VerticalAxis = {|
direction: 'vertical',
line: 'y',
start: 'top',
end: 'bottom',
size: 'height',
crossAxisLine: 'x',
crossAxisStart: 'left',
crossAxisEnd: 'right',
crossAxisSize: 'width',
|};
export type HorizontalAxis = {|
direction: 'horizontal',
line: 'x',
start: 'left',
end: 'right',
size: 'width',
crossAxisLine: 'y',
crossAxisStart: 'top',
crossAxisEnd: 'bottom',
crossAxisSize: 'height',
|};
export type Axis = VerticalAxis | HorizontalAxis;
export type ScrollSize = {|
scrollHeight: number,
scrollWidth: number,
|};
export type ScrollDetails = {|
initial: Position,
current: Position,
// the maximum allowable scroll for the frame
max: Position,
diff: {|
value: Position,
// The actual displacement as a result of a scroll is in the opposite
// direction to the scroll itself. When scrolling down items are displaced
// upwards. This value is the negated version of the 'value'
displacement: Position,
|},
|};
export type Placeholder = {|
client: BoxModel,
tagName: string,
display: string,
|};
export type DraggableDimension = {|
descriptor: DraggableDescriptor,
// the placeholder for the draggable
placeholder: Placeholder,
// relative to the viewport when the drag started
client: BoxModel,
// relative to the whole page
page: BoxModel,
// how much displacement the draggable causes
// this is the size of the marginBox
displaceBy: Position,
|};
export type Scrollable = {|
// This is the window through which the droppable is observed
// It does not change during a drag
pageMarginBox: Rect,
// Used for comparision with dynamic recollecting
frameClient: BoxModel,
scrollSize: ScrollSize,
// Whether or not we should clip the subject by the frame
// Is controlled by the ignoreContainerClipping prop
shouldClipSubject: boolean,
scroll: ScrollDetails,
|};
export type PlaceholderInSubject = {|
// might not actually be increased by
// placeholder if there is no required space
increasedBy: ?Position,
placeholderSize: Position,
// max scroll before placeholder added
// will be null if there was no frame
oldFrameMaxScroll: ?Position,
|};
export type DroppableSubject = {|
// raw, unchanging
page: BoxModel,
withPlaceholder: ?PlaceholderInSubject,
// The hitbox for a droppable
// - page margin box
// - with scroll changes
// - with any additional droppable placeholder
// - clipped by frame
// The subject will be null if the hit area is completely empty
active: ?Rect,
|};
export type DroppableDimension = {|
descriptor: DroppableDescriptor,
axis: Axis,
isEnabled: boolean,
isCombineEnabled: boolean,
// relative to the current viewport
client: BoxModel,
// relative to the whole page
isFixedOnPage: boolean,
// relative to the page
page: BoxModel,
// The container of the droppable
frame: ?Scrollable,
// what is visible through the frame
subject: DroppableSubject,
|};
export type DraggableLocation = {|
droppableId: DroppableId,
index: number,
|};
export type DraggableDimensionMap = { [key: DraggableId]: DraggableDimension };
export type DroppableDimensionMap = { [key: DroppableId]: DroppableDimension };
export type Displacement = {|
draggableId: DraggableId,
isVisible: boolean,
shouldAnimate: boolean,
|};
export type DisplacementMap = { [key: DraggableId]: Displacement };
export type DisplacedBy = {|
value: number,
point: Position,
|};
export type DragMovement = {|
// The draggables that need to move in response to a drag.
// Ordered by closest draggable to the *current* location of the dragging item
displaced: Displacement[],
// displaced as a map
map: DisplacementMap,
displacedBy: DisplacedBy,
|};
export type VerticalUserDirection = 'up' | 'down';
export type HorizontalUserDirection = 'left' | 'right';
export type UserDirection = {|
vertical: VerticalUserDirection,
horizontal: HorizontalUserDirection,
|};
export type Combine = {|
draggableId: DraggableId,
droppableId: DroppableId,
|};
export type CombineImpact = {|
// This has an impact on the hitbox for a grouping action
whenEntered: UserDirection,
combine: Combine,
|};
export type DragImpact = {|
movement: DragMovement,
// a reorder location
destination: ?DraggableLocation,
// a merge location
merge: ?CombineImpact,
|};
export type ClientPositions = {|
// where the user initially selected
// This point is not used to calculate the impact of a dragging item
// It is used to calculate the offset from the initial selection point
selection: Position,
// the current center of the item
borderBoxCenter: Position,
// how far the item has moved from its original position
offset: Position,
|};
export type PagePositions = {|
selection: Position,
borderBoxCenter: Position,
|};
// There are two seperate modes that a drag can be in
// FLUID: everything is done in response to highly granular input (eg mouse)
// SNAP: items move in response to commands (eg keyboard);
export type MovementMode = 'FLUID' | 'SNAP';
export type DragPositions = {|
client: ClientPositions,
page: PagePositions,
|};
// published when a drag starts
export type DragStart = {|
draggableId: DraggableId,
type: TypeId,
source: DraggableLocation,
mode: MovementMode,
|};
export type DragUpdate = {|
...DragStart,
// may not have any destination (drag to nowhere)
destination: ?DraggableLocation,
// populated when a draggable is dragging over another in combine mode
combine: ?Combine,
|};
export type DropReason = 'DROP' | 'CANCEL';
// published when a drag finishes
export type DropResult = {|
...DragUpdate,
reason: DropReason,
|};
export type ScrollOptions = {|
shouldPublishImmediately: boolean,
|};
// using the draggable id rather than the descriptor as the descriptor
// may change as a result of the initial flush. This means that the lift
// descriptor may not be the same as the actual descriptor. To avoid
// confusion the request is just an id which is looked up
// in the dimension-marshal post-flush
// Not including droppableId as it might change in a drop flush
export type LiftRequest = {|
draggableId: DraggableId,
scrollOptions: ScrollOptions,
|};
export type Critical = {|
draggable: DraggableDescriptor,
droppable: DroppableDescriptor,
|};
export type Viewport = {|
// live updates with the latest values
frame: Rect,
scroll: ScrollDetails,
|};
export type DimensionMap = {|
draggables: DraggableDimensionMap,
droppables: DroppableDimensionMap,
|};
export type Published = {|
additions: DraggableDimension[],
removals: DraggableId[],
modified: DroppableDimension[],
|};
export type CompletedDrag = {|
critical: Critical,
result: DropResult,
impact: DragImpact,
|};
export type IdleState = {|
phase: 'IDLE',
completed: ?CompletedDrag,
shouldFlush: boolean,
|};
export type DraggableIdMap = {
[id: DraggableId]: true,
};
export type OnLift = {|
wasDisplaced: DraggableIdMap,
displacedBy: DisplacedBy,
|};
export type DraggingState = {|
phase: 'DRAGGING',
isDragging: true,
critical: Critical,
movementMode: MovementMode,
dimensions: DimensionMap,
initial: DragPositions,
current: DragPositions,
userDirection: UserDirection,
impact: DragImpact,
viewport: Viewport,
onLift: OnLift,
onLiftImpact: DragImpact,
// when there is a fixed list we want to opt out of this behaviour
isWindowScrollAllowed: boolean,
// if we need to jump the scroll (keyboard dragging)
scrollJumpRequest: ?Position,
// whether or not draggable movements should be animated
forceShouldAnimate: ?boolean,
|};
// While dragging we can enter into a bulk collection phase
// During this phase no drag updates are permitted.
// If a drop occurs during this phase, it must wait until it is
// completed before continuing with the drop
// TODO: rename to BulkCollectingState
export type CollectingState = {|
...DraggingState,
phase: 'COLLECTING',
|};
// If a drop action occurs during a bulk collection we need to
// wait for the collection to finish before performing the drop.
// This is to ensure that everything has the correct index after
// a drop
export type DropPendingState = {|
...DraggingState,
phase: 'DROP_PENDING',
isWaiting: boolean,
reason: DropReason,
|};
// An optional phase for animating the drop / cancel if it is needed
export type DropAnimatingState = {|
phase: 'DROP_ANIMATING',
completed: CompletedDrag,
newHomeClientOffset: Position,
dropDuration: number,
// We still need to render placeholders and fix the dimensions of the dragging item
dimensions: DimensionMap,
|};
export type State =
| IdleState
| DraggingState
| CollectingState
| DropPendingState
| DropAnimatingState;
export type StateWhenUpdatesAllowed = DraggingState | CollectingState;
export type Announce = (message: string) => void;
export type InOutAnimationMode = 'none' | 'open' | 'close';
export type ResponderProvided = {|
announce: Announce,
|};
export type OnBeforeDragStartResponder = (start: DragStart) => mixed;
export type OnDragStartResponder = (
start: DragStart,
provided: ResponderProvided,
) => mixed;
export type OnDragUpdateResponder = (
update: DragUpdate,
provided: ResponderProvided,
) => mixed;
export type OnDragEndResponder = (
result: DropResult,
provided: ResponderProvided,
) => mixed;
export type Responders = {|
onBeforeDragStart?: OnBeforeDragStartResponder,
onDragStart?: OnDragStartResponder,
onDragUpdate?: OnDragUpdateResponder,
// always required
onDragEnd: OnDragEndResponder,
|};