react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
398 lines (336 loc) • 10.8 kB
JavaScript
// @flow
import type { Store as ReduxStore, Dispatch as ReduxDispatch } from 'redux';
import type { Action as ActionCreators } from './state/action-creators';
export type Id = string;
export type DraggableId = Id;
export type DroppableId = Id;
export type TypeId = Id;
export type ZIndex = number | string;
export type DroppableDescriptor = {|
id: DroppableId,
type: TypeId,
|}
export type DraggableDescriptor = {|
id: DraggableId,
droppableId: DroppableId,
index: number,
|}
export type Position = {|
x: number,
y: number,
|};
// Kept as a loose type so that functions can
// accept Spacing and receive an Area or a Spacing
export type Spacing = {
top: number,
right: number,
bottom: number,
left: number,
}
export type Area = {|
top: number,
right: number,
bottom: number,
left: number,
// additions to Spacing
width: number,
height: number,
center: Position,
|}
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 Placeholder = {|
// We apply the margin separately to maintain margin collapsing
// behavior of the original element
borderBox: Area,
margin: Spacing,
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: {|
marginBox: Area,
borderBox: Area,
|},
// relative to the whole page
page: {|
marginBox: Area,
borderBox: Area,
|},
|}
export type ClosestScrollable = {|
// This is the window through which the droppable is observed
// It does not change during a drag
frame: Area,
// Whether or not we should clip the subject by the frame
// Is controlled by the ignoreContainerClipping prop
shouldClipSubject: boolean,
scroll: {|
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 DroppableDimensionViewport = {|
// will be null if there is no closest scrollable
closestScrollable: ?ClosestScrollable,
subject: Area,
// this is the subject through the viewport of the frame (if applicable)
// it also takes into account any changes to the viewport scroll
// clipped area will be null if it is completely outside of the frame and frame clipping is on
clipped: ?Area,
|}
/*
# The CSS box model
> https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Introduction_to_the_CSS_box_model
------------------------------------
| MARGIN | (marginBox)
| ------------------------------ |
| | BORDER | | (borderBox)
| | ------------------------ | |
| | | PADDING | | | (paddingBox) - not used by anything really
| | | ------------------ | | |
| | | | CONTENT | | | | (contentBox)
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | ------------------ | | |
| | | | | |
| | ------------------------ | |
| | | |
| ------------------------------ |
| |
|----------------------------------|
Example (https://codepen.io/alexreardon/pen/oqRBEq)
- width: 100px;
- padding: 10px;
- border: 5px;
## box-sizing: content-box;
Any 'width' property is set on the contextBox and padding and border is adding on top
So the borderBox width would be 130px (border: 5px * 2 + padding: 10px * 2 + content 100px)
## box-sizing: border-box;
Any 'width' property is set on the borderBox
So the borderBox width would be 100px and the contentBox would be 70px
100 - (border: 5px * 2 + padding: 10px * 2)
## Element.getBoundingClientRect()
This always returns the borderBox sizes, regardless of box-sizing.
- for content-box with would be 130px
- for border-box it would be 100px
*/
export type BoxModel = {|
// the borderBox with margin added (outer)
marginBox: Area,
// the element inclusive of padding and border
borderBox: Area,
// the element without margin or padding or border (inner)
contentBox: Area,
|}
export type DroppableDimension = {|
descriptor: DroppableDescriptor,
axis: Axis,
isEnabled: boolean,
// relative to the current viewport
client: BoxModel,
// relative to the whole page
page: BoxModel,
// The container of the droppable
viewport: DroppableDimensionViewport,
|}
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 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[],
amount: Position,
// is moving forward relative to the starting position
// TODO: rename to 'shouldDisplaceForward'?
isBeyondStartPosition: boolean,
|}
export type DragImpact = {|
movement: DragMovement,
// the direction of the Droppable you are over
direction: ?Direction,
destination: ?DraggableLocation,
|}
export type InitialDragPositions = {|
// where the user initially selected
selection: Position,
// the current center of the item
center: Position,
|}
// When dragging with a pointer such as a mouse or touch input we want to automatically
// scroll user the under input when we get near the bottom of a Droppable or the window.
// When Dragging with a keyboard we want to jump as required
export type AutoScrollMode = 'FLUID' | 'JUMP';
export type Viewport = {|
scroll: Position,
maxScroll: Position,
subject: Area,
|}
export type InitialDrag = {|
descriptor: DraggableDescriptor,
autoScrollMode: AutoScrollMode,
// relative to the viewport when the drag started
client: InitialDragPositions,
// viewport + window scroll (position relative to 0, 0)
page: InitialDragPositions,
// Storing viewport directly to support movement during a window scroll.
// Value required for comparison with current scroll
viewport: Viewport,
|}
export type CurrentDragPositions = {|
...InitialDragPositions,
// how far the item has moved from its original position
offset: Position,
|}
export type CurrentDrag = {|
// viewport
client: CurrentDragPositions,
// viewport + scroll
page: CurrentDragPositions,
// whether or not draggable movements should be animated
shouldAnimate: boolean,
// We do not want to calculate drag impacts until we have completed
// the first bulk publish. Otherwise the onDragUpdate hook will
// be called with incorrect indexes.
// Before the first bulk publish the calculations will return incorrect indexes.
hasCompletedFirstBulkPublish: boolean,
viewport: Viewport
|}
// published when a drag starts
export type DragStart = {|
draggableId: DraggableId,
type: TypeId,
source: DraggableLocation,
|}
export type DragUpdate = {|
...DragStart,
// may not have any destination (drag to nowhere)
destination: ?DraggableLocation,
|}
export type DropReason = 'DROP' | 'CANCEL';
// published when a drag finishes
export type DropResult = {|
...DragUpdate,
reason: DropReason,
|}
export type DragState = {|
initial: InitialDrag,
current: CurrentDrag,
impact: DragImpact,
// if we need to jump the scroll (keyboard dragging)
scrollJumpRequest: ?Position,
|}
export type PendingDrop = {|
newHomeOffset: Position,
impact: DragImpact,
result: DropResult,
|}
export type Phase =
// The application rest state
'IDLE' |
// When a drag starts we need to flush any existing animations
// that might be occurring. While this flush is occurring we
// are in this phase
'PREPARING' |
// After the animations have been flushed we need to collect the
// dimensions of all of the Draggable and Droppable components.
// At this point a drag has not started yet and the onDragStart
// hook has not fired.
'COLLECTING_INITIAL_DIMENSIONS' |
// A drag is active. The onDragStart hook has been fired
'DRAGGING' |
// An optional phase for animating the drop / cancel if it is needed
'DROP_ANIMATING' |
// The final state of a drop / cancel.
// This will result in the onDragEnd hook being fired
'DROP_COMPLETE';
export type ScrollOptions = {|
shouldPublishImmediately: boolean,
|}
export type LiftRequest = {|
draggableId: DraggableId,
scrollOptions: ScrollOptions,
|}
export type DimensionState = {|
// 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
request: ?LiftRequest,
draggable: DraggableDimensionMap,
droppable: DroppableDimensionMap,
|};
export type DropState = {|
pending: ?PendingDrop,
result: ?DropResult,
|}
export type State = {|
phase: Phase,
dimension: DimensionState,
// null if not dragging
drag: ?DragState,
// available when dropping or cancelling
drop: ?DropState,
|};
export type Action = ActionCreators;
export type Dispatch = ReduxDispatch<Action>;
export type Store = ReduxStore<State, Action, Dispatch>;
export type Announce = (message: string) => void;
export type HookProvided = {|
announce: Announce,
|}
export type OnDragStartHook = (start: DragStart, provided: HookProvided) => void;
export type OnDragUpdateHook = (update: DragUpdate, provided: HookProvided) => void;
export type OnDragEndHook = (result: DropResult, provided: HookProvided) => void;
export type Hooks = {|
onDragStart?: OnDragStartHook,
onDragUpdate?: OnDragUpdateHook,
// always required
onDragEnd: OnDragEndHook,
|}