react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
223 lines (198 loc) • 7.8 kB
JSX
// @flow
import React, { type Node } from 'react';
import PropTypes from 'prop-types';
import createStore from '../../state/create-store';
import createHookCaller from '../../state/hooks/hook-caller';
import createDimensionMarshal from '../../state/dimension-marshal/dimension-marshal';
import createStyleMarshal, { resetStyleContext } from '../style-marshal/style-marshal';
import canStartDrag from '../../state/can-start-drag';
import scrollWindow from '../window/scroll-window';
import createAnnouncer from '../announcer/announcer';
import createAutoScroller from '../../state/auto-scroller';
import type { Announcer } from '../announcer/announcer-types';
import type { AutoScroller } from '../../state/auto-scroller/auto-scroller-types';
import type { StyleMarshal } from '../style-marshal/style-marshal-types';
import type {
DimensionMarshal,
Callbacks as DimensionMarshalCallbacks,
} from '../../state/dimension-marshal/dimension-marshal-types';
import type {
DraggableId,
Store,
State,
DraggableDimension,
DroppableDimension,
DroppableId,
Position,
Hooks,
Viewport,
} from '../../types';
import type {
HookCaller,
} from '../../state/hooks/hooks-types';
import {
storeKey,
dimensionMarshalKey,
styleContextKey,
canLiftContextKey,
} from '../context-keys';
import {
clean,
move,
publishDraggableDimension,
publishDroppableDimension,
updateDroppableDimensionScroll,
updateDroppableDimensionIsEnabled,
bulkPublishDimensions,
} from '../../state/action-creators';
type Props = {|
...Hooks,
children: ?Node,
|}
type Context = {
[string]: Store
}
// Reset any context that gets persisted across server side renders
export const resetServerContext = () => {
resetStyleContext();
};
export default class DragDropContext extends React.Component<Props> {
/* eslint-disable react/sort-comp */
store: Store
dimensionMarshal: DimensionMarshal
styleMarshal: StyleMarshal
autoScroller: AutoScroller
hookCaller: HookCaller
announcer: Announcer
unsubscribe: Function
constructor(props: Props, context: mixed) {
super(props, context);
this.store = createStore();
this.announcer = createAnnouncer();
// create the hook caller
this.hookCaller = createHookCaller(this.announcer.announce);
// create the style marshal
this.styleMarshal = createStyleMarshal();
// create the dimension marshal
const callbacks: DimensionMarshalCallbacks = {
cancel: () => {
this.store.dispatch(clean());
},
publishDraggable: (dimension: DraggableDimension) => {
this.store.dispatch(publishDraggableDimension(dimension));
},
publishDroppable: (dimension: DroppableDimension) => {
this.store.dispatch(publishDroppableDimension(dimension));
},
bulkPublish: (droppables: DroppableDimension[], draggables: DraggableDimension[]) => {
this.store.dispatch(bulkPublishDimensions(droppables, draggables));
},
updateDroppableScroll: (id: DroppableId, newScroll: Position) => {
this.store.dispatch(updateDroppableDimensionScroll(id, newScroll));
},
updateDroppableIsEnabled: (id: DroppableId, isEnabled: boolean) => {
this.store.dispatch(updateDroppableDimensionIsEnabled(id, isEnabled));
},
};
this.dimensionMarshal = createDimensionMarshal(callbacks);
this.autoScroller = createAutoScroller({
scrollWindow,
scrollDroppable: this.dimensionMarshal.scrollDroppable,
move: (
id: DraggableId,
client: Position,
viewport: Viewport,
shouldAnimate?: boolean
): void => {
this.store.dispatch(move(id, client, viewport, shouldAnimate));
},
});
let previous: State = this.store.getState();
this.unsubscribe = this.store.subscribe(() => {
const current = this.store.getState();
const previousInThisExecution: State = previous;
const isPhaseChanging: boolean = current.phase !== previous.phase;
// setting previous now rather than at the end of this function
// incase a function is called that syncorously causes a state update
// which will re-invoke this function before it has completed a previous
// invocation.
previous = current;
// Style updates do not cause more actions. It is important to update styles
// before hooks are called: specifically the onDragEnd hook. We need to clear
// the transition styles off the elements before a reorder to prevent strange
// post drag animations in firefox. Even though we clear the transition off
// a Draggable - if it is done after a reorder firefox will still apply the
// transition.
if (isPhaseChanging) {
this.styleMarshal.onPhaseChange(current);
}
const isDragEnding: boolean = previousInThisExecution.phase === 'DRAGGING' && current.phase !== 'DRAGGING';
// in the case that a drag is ending we need to instruct the dimension marshal
// to stop listening to changes. Otherwise it will try to process
// changes after the reorder in onDragEnd
if (isDragEnding) {
this.dimensionMarshal.onPhaseChange(current);
}
// We recreate the Hook object so that consumers can pass in new
// hook props at any time (eg if they are using arrow functions)
const hooks: Hooks = {
onDragStart: this.props.onDragStart,
onDragEnd: this.props.onDragEnd,
onDragUpdate: this.props.onDragUpdate,
};
this.hookCaller.onStateChange(hooks, previousInThisExecution, current);
// The following two functions are dangerous. They can both syncronously
// create new actions that update the application state. That will cause
// this subscription function to be called again before the next line is called.
// if isDragEnding we have already called the marshal
if (isPhaseChanging && !isDragEnding) {
this.dimensionMarshal.onPhaseChange(current);
}
// We could block this action from being called if this function has been reinvoked
// before completing and dragging and autoScrollMode === 'FLUID'.
// However, it is not needed at this time
this.autoScroller.onStateChange(previousInThisExecution, current);
});
}
// Need to declare childContextTypes without flow
// https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/22
static childContextTypes = {
[storeKey]: PropTypes.shape({
dispatch: PropTypes.func.isRequired,
subscribe: PropTypes.func.isRequired,
getState: PropTypes.func.isRequired,
}).isRequired,
[dimensionMarshalKey]: PropTypes.object.isRequired,
[styleContextKey]: PropTypes.string.isRequired,
[canLiftContextKey]: PropTypes.func.isRequired,
}
/* eslint-enable */
getChildContext(): Context {
return {
[storeKey]: this.store,
[dimensionMarshalKey]: this.dimensionMarshal,
[styleContextKey]: this.styleMarshal.styleContext,
[canLiftContextKey]: this.canLift,
};
}
// Providing function on the context for drag handles to use to
// let them know if they can start a drag or not. This is done
// rather than mapping a prop onto the drag handle so that we
// do not need to re-render a connected drag handle in order to
// pull this state off. It would cause a re-render of all items
// on drag start which is too expensive.
// This is useful when the user
canLift = (id: DraggableId) => canStartDrag(this.store.getState(), id);
componentDidMount() {
this.styleMarshal.mount();
this.announcer.mount();
}
componentWillUnmount() {
this.unsubscribe();
this.styleMarshal.unmount();
this.announcer.unmount();
}
render() {
return this.props.children;
}
}