// @flow
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import memoizeOne from 'memoize-one';
import invariant from 'invariant';
import type {
Position,
HTMLElement,
DraggableDimension,
InitialDragLocation,
} from '../../types';
import DraggableDimensionPublisher from '../draggable-dimension-publisher/';
import Moveable from '../moveable/';
import DragHandle from '../drag-handle';
import { css } from '../animation';
import getWindowScrollPosition from '../get-window-scroll-position';
// eslint-disable-next-line no-duplicate-imports
import type {
Callbacks as DragHandleCallbacks,
Provided as DragHandleProvided,
} from '../drag-handle/drag-handle-types';
import getCenterPosition from '../get-center-position';
import Placeholder from '../placeholder';
import { droppableIdKey } from '../context-keys';
import type {
Props,
Provided,
StateSnapshot,
DefaultProps,
DraggingStyle,
NotDraggingStyle,
DraggableStyle,
ZIndexOptions,
} from './draggable-types';
import type { Speed, Style as MovementStyle } from '../moveable/moveable-types';
type State = {|
ref: ?HTMLElement,
|}
export const zIndexOptions: ZIndexOptions = {
dragging: 5000,
dropAnimating: 4500,
};
export default class Draggable extends Component {
/* eslint-disable react/sort-comp */
props: Props
state: State
callbacks: DragHandleCallbacks
state: State = {
ref: null,
}
static defaultProps: DefaultProps = {
isDragDisabled: false,
type: 'DEFAULT',
}
// Need to declare contextTypes without flow
// https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22
static contextTypes = {
[droppableIdKey]: PropTypes.string.isRequired,
}
/* eslint-enable */
constructor(props: Props, context: mixed) {
super(props, context);
const callbacks: DragHandleCallbacks = {
onLift: this.onLift,
onMove: this.onMove,
onDrop: this.onDrop,
onCancel: this.onCancel,
onMoveBackward: this.onMoveBackward,
onMoveForward: this.onMoveForward,
onCrossAxisMoveForward: this.onCrossAxisMoveForward,
onCrossAxisMoveBackward: this.onCrossAxisMoveBackward,
onWindowScroll: this.onWindowScroll,
};
this.callbacks = callbacks;
}
// This should already be handled gracefully in DragHandle.
// Just being extra clear here
throwIfCannotDrag() {
invariant(this.state.ref,
'Draggable: cannot drag as no DOM node has been provided'
);
invariant(!this.props.isDragDisabled,
'Draggable: cannot drag as dragging is not enabled'
);
}
onMoveEnd = () => {
if (!this.props.isDropAnimating) {
return;
}
this.props.dropAnimationFinished(this.props.draggableId);
}
onLift = (options: {client: Position, isScrollAllowed: boolean}) => {
this.throwIfCannotDrag();
const { client, isScrollAllowed } = options;
const { lift, draggableId, type } = this.props;
const { ref } = this.state;
const initial: InitialDragLocation = {
selection: client,
center: getCenterPosition(ref),
};
const windowScroll: Position = getWindowScrollPosition();
lift(draggableId, type, initial, windowScroll, isScrollAllowed);
}
onMove = (client: Position) => {
this.throwIfCannotDrag();
const { draggableId, dimension, move } = this.props;
// dimensions not provided yet
if (!dimension) {
return;
}
const windowScroll: Position = getWindowScrollPosition();
move(draggableId, client, windowScroll);
}
onMoveForward = () => {
this.throwIfCannotDrag();
this.props.moveForward(this.props.draggableId);
}
onMoveBackward = () => {
this.throwIfCannotDrag();
this.props.moveBackward(this.props.draggableId);
}
onCrossAxisMoveForward = () => {
this.throwIfCannotDrag();
this.props.crossAxisMoveForward(this.props.draggableId);
}
onCrossAxisMoveBackward = () => {
this.throwIfCannotDrag();
this.props.crossAxisMoveBackward(this.props.draggableId);
}
onWindowScroll = () => {
this.throwIfCannotDrag();
const windowScroll = getWindowScrollPosition();
this.props.moveByWindowScroll(this.props.draggableId, windowScroll);
}
onDrop = () => {
this.throwIfCannotDrag();
this.props.drop();
}
onCancel = () => {
// Not checking if drag is enabled.
// Cancel is an escape mechanism
this.props.cancel();
}
// React calls ref callback twice for every render
// https://github.com/facebook/react/pull/8333/files
setRef = ((ref: ?HTMLElement) => {
// TODO: need to clear this.state.ref on unmount
if (ref === null) {
return;
}
if (ref === this.state.ref) {
return;
}
// need to trigger a child render when ref changes
this.setState({
ref,
});
})
getDraggableRef = (): ?HTMLElement => this.state.ref;
getPlaceholder() {
const dimension: ?DraggableDimension = this.props.dimension;
invariant(dimension, 'cannot get a drag placeholder when not dragging');
return (
);
}
getDraggingStyle = memoizeOne(
(width: number,
height: number,
top: number,
left: number,
isDropAnimating: boolean,
movementStyle: MovementStyle): DraggingStyle => {
// For an explanation of properties see `draggable-types`.
const style: DraggingStyle = {
position: 'fixed',
boxSizing: 'border-box',
pointerEvents: 'none',
zIndex: isDropAnimating ? zIndexOptions.dropAnimating : zIndexOptions.dragging,
width,
height,
top,
left,
margin: 0,
transform: movementStyle.transform ? `${movementStyle.transform}` : null,
// base style
WebkitTouchCallout: 'none',
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
touchAction: 'none',
};
return style;
}
)
getNotDraggingStyle = memoizeOne(
(
canAnimate: boolean,
movementStyle: MovementStyle,
canLift: boolean,
): NotDraggingStyle => {
const style: NotDraggingStyle = {
transition: canAnimate ? css.outOfTheWay : null,
transform: movementStyle.transform,
pointerEvents: canLift ? 'auto' : 'none',
// base style
WebkitTouchCallout: 'none',
WebkitTapHighlightColor: 'rgba(0,0,0,0)',
touchAction: 'none',
};
return style;
}
)
getProvided = memoizeOne(
(
isDragging: boolean,
isDropAnimating: boolean,
canLift: boolean,
canAnimate: boolean,
dimension: ?DraggableDimension,
dragHandleProps: ?DragHandleProvided,
movementStyle: MovementStyle,
): Provided => {
const useDraggingStyle: boolean = isDragging || isDropAnimating;
const draggableStyle: DraggableStyle = (() => {
if (!useDraggingStyle) {
return this.getNotDraggingStyle(
canAnimate,
movementStyle,
canLift,
);
}
invariant(dimension, 'draggable dimension required for dragging');
// Margins are accounted for. See `draggable-types` for explanation
const { width, height, top, left } = dimension.client.withoutMargin;
return this.getDraggingStyle(width, height, top, left, isDropAnimating, movementStyle);
})();
const provided: Provided = {
innerRef: this.setRef,
placeholder: useDraggingStyle ? this.getPlaceholder() : null,
dragHandleProps,
draggableStyle,
};
return provided;
}
)
getSnapshot = memoizeOne((isDragging: boolean, isDropAnimating: boolean): StateSnapshot => ({
isDragging: (isDragging || isDropAnimating),
}))
getSpeed = memoizeOne(
(isDragging: boolean, isDropAnimating: boolean, canAnimate: boolean): Speed => {
if (!canAnimate) {
return 'INSTANT';
}
if (isDropAnimating) {
return 'STANDARD';
}
// if dragging and can animate - then move quickly
if (isDragging) {
return 'FAST';
}
// Moving out of the way.
// Animation taken care of by css
return 'INSTANT';
})
render() {
const {
draggableId,
type,
offset,
isDragging,
isDropAnimating,
canLift,
canAnimate,
isDragDisabled,
dimension,
children,
direction,
} = this.props;
const speed = this.getSpeed(isDragging, isDropAnimating, canAnimate);
return (
{(movementStyle: MovementStyle) => (
{(dragHandleProps: ?DragHandleProvided) =>
children(
this.getProvided(
isDragging,
isDropAnimating,
canLift,
canAnimate,
dimension,
dragHandleProps,
movementStyle,
),
this.getSnapshot(isDragging, isDropAnimating)
)
}
)}
);
}
}