UNPKG

react-beautiful-dnd

Version:

Beautiful, accessible drag and drop for lists with React.js

365 lines (323 loc) 9.87 kB
// @flow import React, { Component, Fragment, type Node } from 'react'; import { type Position, type BoxModel } from 'css-box-model'; import PropTypes from 'prop-types'; import memoizeOne from 'memoize-one'; import invariant from 'tiny-invariant'; import { isEqual, origin } from '../../state/position'; import type { DraggableDimension, ItemPositions, DroppableId, AutoScrollMode, TypeId, } from '../../types'; import DraggableDimensionPublisher from '../draggable-dimension-publisher'; import Moveable from '../moveable'; import DragHandle from '../drag-handle'; import getViewport from '../window/get-viewport'; import type { DragHandleProps, Callbacks as DragHandleCallbacks, } from '../drag-handle/drag-handle-types'; import getBorderBoxCenterPosition from '../get-border-box-center-position'; import Placeholder from '../placeholder'; import { droppableIdKey, styleContextKey, droppableTypeKey, } from '../context-keys'; import * as timings from '../../debug/timings'; import type { Props, Provided, StateSnapshot, DraggingStyle, NotDraggingStyle, DraggableStyle, ZIndexOptions, } from './draggable-types'; import getWindowScroll from '../window/get-window-scroll'; import throwIfRefIsInvalid from '../throw-if-invalid-inner-ref'; import type { Speed } from '../moveable/moveable-types'; export const zIndexOptions: ZIndexOptions = { dragging: 5000, dropAnimating: 4500, }; const getTranslate = (offset: Position): ?string => { // we do not translate to origin // we simply clear the translate if (isEqual(offset, origin)) { return null; } return `translate(${offset.x}px, ${offset.y}px)`; }; const getSpeed = ( isDragging: boolean, shouldAnimateDragMovement: boolean, isDropAnimating: boolean, ): Speed => { if (isDropAnimating) { return 'STANDARD'; } if (isDragging && shouldAnimateDragMovement) { return 'FAST'; } // if dragging: no animation // if not dragging: animation done with CSS return 'INSTANT'; }; export default class Draggable extends Component<Props> { /* eslint-disable react/sort-comp */ callbacks: DragHandleCallbacks; styleContext: string; ref: ?HTMLElement = null; // Need to declare contextTypes without flow // https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22 static contextTypes = { [droppableIdKey]: PropTypes.string.isRequired, [droppableTypeKey]: PropTypes.string.isRequired, [styleContextKey]: PropTypes.string.isRequired, }; constructor(props: Props, context: Object) { super(props, context); const callbacks: DragHandleCallbacks = { onLift: this.onLift, onMove: (clientSelection: Position) => props.move({ client: clientSelection, shouldAnimate: false }), onDrop: () => props.drop({ reason: 'DROP' }), onCancel: () => props.drop({ reason: 'CANCEL' }), onMoveUp: props.moveUp, onMoveDown: props.moveDown, onMoveRight: props.moveRight, onMoveLeft: props.moveLeft, onWindowScroll: () => props.moveByWindowScroll({ scroll: getWindowScroll() }), }; this.callbacks = callbacks; this.styleContext = context[styleContextKey]; } componentWillUnmount() { // releasing reference to ref for cleanup this.ref = null; } onMoveEnd = () => { if (this.props.isDropAnimating) { this.props.dropAnimationFinished(); } }; onLift = (options: { clientSelection: Position, autoScrollMode: AutoScrollMode, }) => { timings.start('LIFT'); const ref: ?HTMLElement = this.ref; invariant(ref); invariant( !this.props.isDragDisabled, 'Cannot lift a Draggable when it is disabled', ); const { clientSelection, autoScrollMode } = options; const { lift, draggableId } = this.props; const client: ItemPositions = { selection: clientSelection, borderBoxCenter: getBorderBoxCenterPosition(ref), offset: origin, }; lift({ id: draggableId, client, autoScrollMode, viewport: getViewport(), }); timings.finish('LIFT'); }; // React calls ref callback twice for every render // https://github.com/facebook/react/pull/8333/files setRef = (ref: ?HTMLElement) => { if (ref === null) { return; } if (ref === this.ref) { return; } // At this point the ref has been changed or initially populated this.ref = ref; throwIfRefIsInvalid(ref); }; getDraggableRef = (): ?HTMLElement => this.ref; getDraggingStyle = memoizeOne( ( change: Position, dimension: DraggableDimension, isDropAnimating: boolean, ): DraggingStyle => { const box: BoxModel = dimension.client; const style: DraggingStyle = { // ## Placement position: 'fixed', // As we are applying the margins we need to align to the start of the marginBox top: box.marginBox.top, left: box.marginBox.left, // ## Sizing // Locking these down as pulling the node out of the DOM could cause it to change size boxSizing: 'border-box', width: box.borderBox.width, height: box.borderBox.height, // ## Movement // Opting out of the standard css transition for the dragging item transition: 'none', // Layering zIndex: isDropAnimating ? zIndexOptions.dropAnimating : zIndexOptions.dragging, // Moving in response to user input transform: getTranslate(change), // ## Performance pointerEvents: 'none', }; return style; }, ); getNotDraggingStyle = memoizeOne( ( current: Position, shouldAnimateDisplacement: boolean, ): NotDraggingStyle => { const style: NotDraggingStyle = { transform: getTranslate(current), // use the global animation for animation - or opt out of it transition: shouldAnimateDisplacement ? null : 'none', // transition: css.outOfTheWay, }; return style; }, ); getProvided = memoizeOne( ( change: Position, isDragging: boolean, isDropAnimating: boolean, shouldAnimateDisplacement: boolean, dimension: ?DraggableDimension, dragHandleProps: ?DragHandleProps, ): Provided => { const useDraggingStyle: boolean = isDragging || isDropAnimating; const draggableStyle: DraggableStyle = (() => { if (!useDraggingStyle) { return this.getNotDraggingStyle(change, shouldAnimateDisplacement); } invariant(dimension, 'draggable dimension required for dragging'); // Need to position element in original visual position. To do this // we position it without return this.getDraggingStyle(change, dimension, isDropAnimating); })(); const provided: Provided = { innerRef: this.setRef, draggableProps: { 'data-react-beautiful-dnd-draggable': this.styleContext, style: draggableStyle, }, dragHandleProps, }; return provided; }, ); getSnapshot = memoizeOne( ( isDragging: boolean, isDropAnimating: boolean, draggingOver: ?DroppableId, ): StateSnapshot => ({ isDragging: isDragging || isDropAnimating, isDropAnimating, draggingOver, }), ); renderChildren = ( change: Position, dragHandleProps: ?DragHandleProps, ): ?Node => { const { isDragging, isDropAnimating, dimension, draggingOver, shouldAnimateDisplacement, children, } = this.props; const child: ?Node = children( this.getProvided( change, isDragging, isDropAnimating, shouldAnimateDisplacement, dimension, dragHandleProps, ), this.getSnapshot(isDragging, isDropAnimating, draggingOver), ); const isDraggingOrDropping: boolean = isDragging || isDropAnimating; const placeholder: ?Node = (() => { if (!isDraggingOrDropping) { return null; } invariant(dimension, 'Draggable: Dimension is required for dragging'); return <Placeholder placeholder={dimension.placeholder} />; })(); return ( <Fragment> {child} {placeholder} </Fragment> ); }; render() { const { draggableId, index, offset, isDragging, isDropAnimating, isDragDisabled, shouldAnimateDragMovement, disableInteractiveElementBlocking, } = this.props; const droppableId: DroppableId = this.context[droppableIdKey]; const type: TypeId = this.context[droppableTypeKey]; const speed: Speed = getSpeed( isDragging, shouldAnimateDragMovement, isDropAnimating, ); return ( <DraggableDimensionPublisher key={draggableId} draggableId={draggableId} droppableId={droppableId} type={type} index={index} getDraggableRef={this.getDraggableRef} > <Moveable speed={speed} destination={offset} onMoveEnd={this.onMoveEnd}> {(change: Position) => ( <DragHandle draggableId={draggableId} isDragging={isDragging} isDropAnimating={isDropAnimating} isEnabled={!isDragDisabled} callbacks={this.callbacks} getDraggableRef={this.getDraggableRef} // by default we do not allow dragging on interactive elements canDragInteractiveElements={disableInteractiveElementBlocking} > {(dragHandleProps: ?DragHandleProps) => this.renderChildren(change, dragHandleProps) } </DragHandle> )} </Moveable> </DraggableDimensionPublisher> ); } }