react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
210 lines (175 loc) • 6.08 kB
Flow
// @flow
import { Component } from 'react';
import PropTypes from 'prop-types';
import memoizeOne from 'memoize-one';
import type {
Props,
DragHandleProps,
} from './drag-handle-types';
import type {
Sensor,
MouseSensor,
KeyboardSensor,
TouchSensor,
CreateSensorArgs,
} from './sensor/sensor-types';
import { styleContextKey, canLiftContextKey } from '../context-keys';
import shouldAllowDraggingFromTarget from './util/should-allow-dragging-from-target';
import createMouseSensor from './sensor/create-mouse-sensor';
import createKeyboardSensor from './sensor/create-keyboard-sensor';
import createTouchSensor from './sensor/create-touch-sensor';
const getFalse: () => boolean = () => false;
export default class DragHandle extends Component<Props> {
/* eslint-disable react/sort-comp */
mouseSensor: MouseSensor;
keyboardSensor: KeyboardSensor;
touchSensor: TouchSensor;
sensors: Sensor[];
styleContext: string;
canLift: () => boolean;
// Need to declare contextTypes without flow
// https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22
static contextTypes = {
[styleContextKey]: PropTypes.string.isRequired,
[canLiftContextKey]: PropTypes.func.isRequired,
}
constructor(props: Props, context: Object) {
super(props, context);
const args: CreateSensorArgs = {
callbacks: this.props.callbacks,
getDraggableRef: this.props.getDraggableRef,
canStartCapturing: this.canStartCapturing,
};
this.mouseSensor = createMouseSensor(args);
this.keyboardSensor = createKeyboardSensor(args);
this.touchSensor = createTouchSensor(args);
this.sensors = [
this.mouseSensor,
this.keyboardSensor,
this.touchSensor,
];
this.styleContext = context[styleContextKey];
// The canLift function is read directly off the context
// and will communicate with the store. This is done to avoid
// needing to query a property from the store and re-render this component
// with that value. By putting it as a function on the context we are able
// to avoid re-rendering to pass this information while still allowing
// drag-handles to obtain this state if they need it.
this.canLift = context[canLiftContextKey];
}
componentWillUnmount() {
this.sensors.forEach((sensor: Sensor) => {
// kill the current drag and fire a cancel event if
const wasCapturing = sensor.isCapturing();
const wasDragging = sensor.isDragging();
// stop capturing
if (wasCapturing) {
sensor.kill();
}
// cancel if drag was occurring
if (wasDragging) {
this.props.callbacks.onCancel();
}
});
}
componentWillReceiveProps(nextProps: Props) {
const isCapturing: boolean = this.isAnySensorCapturing();
if (!isCapturing) {
return;
}
const isDragStopping: boolean = (this.props.isDragging && !nextProps.isDragging);
// if the application cancels a drag we need to unbind the handlers
if (isDragStopping) {
this.sensors.forEach((sensor: Sensor) => {
if (sensor.isCapturing()) {
sensor.kill();
// not firing any cancel event as the drag is already over
}
});
return;
}
// dragging disabled mid drag
if (!nextProps.isEnabled) {
this.sensors.forEach((sensor: Sensor) => {
if (sensor.isCapturing()) {
const wasDragging: boolean = sensor.isDragging();
// stop listening
sensor.kill();
// we need to cancel the drag if it was dragging
if (wasDragging) {
this.props.callbacks.onCancel();
}
}
});
}
}
onKeyDown = (event: KeyboardEvent) => {
// let the mouse sensor deal with it
if (this.mouseSensor.isCapturing()) {
return;
}
this.keyboardSensor.onKeyDown(event, this.props);
}
onMouseDown = (event: MouseEvent) => {
// let the other sensors deal with it
if (this.keyboardSensor.isCapturing() || this.mouseSensor.isCapturing()) {
return;
}
this.mouseSensor.onMouseDown(event);
}
onTouchStart = (event: TouchEvent) => {
// let the keyboard sensor deal with it
if (this.mouseSensor.isCapturing() || this.keyboardSensor.isCapturing()) {
console.error('mouse or keyboard already listening when attempting to touch drag');
return;
}
this.touchSensor.onTouchStart(event);
}
onTouchMove = (event: TouchEvent) => {
this.touchSensor.onTouchMove(event);
}
onClick = (event: MouseEvent) => {
// The mouse or touch sensor may want to block the click
this.mouseSensor.onClick(event);
this.touchSensor.onClick(event);
}
canStartCapturing = (event: Event) => {
// this might be before a drag has started - isolated to this element
if (this.isAnySensorCapturing()) {
return false;
}
// this will check if anything else in the system is dragging
if (!this.canLift()) {
return false;
}
// check if we are dragging an interactive element
return shouldAllowDraggingFromTarget(event, this.props);
}
isAnySensorDragging = (): boolean =>
this.sensors.some((sensor: Sensor) => sensor.isDragging())
isAnySensorCapturing = (): boolean =>
this.sensors.some((sensor: Sensor) => sensor.isCapturing())
getProvided = memoizeOne((isEnabled: boolean, isDragging: boolean): ?DragHandleProps => {
if (!isEnabled) {
return null;
}
const provided: DragHandleProps = {
onMouseDown: this.onMouseDown,
onKeyDown: this.onKeyDown,
onTouchStart: this.onTouchStart,
onTouchMove: this.onTouchMove,
onClick: this.onClick,
tabIndex: 0,
'aria-grabbed': isDragging,
'data-react-beautiful-dnd-drag-handle': this.styleContext,
draggable: false,
onDragStart: getFalse,
onDrop: getFalse,
};
return provided;
})
render() {
const { children, isEnabled } = this.props;
return children(this.getProvided(isEnabled, this.isAnySensorDragging()));
}
}