diff --git a/components/higher-order/with-dnd/README.md b/components/higher-order/with-dnd/README.md
new file mode 100644
index 0000000000000..a6547ced68e86
--- /dev/null
+++ b/components/higher-order/with-dnd/README.md
@@ -0,0 +1,3 @@
+# withDragAndDrop
+
+// @todo :clk:doc
diff --git a/components/higher-order/with-dnd/index.js b/components/higher-order/with-dnd/index.js
new file mode 100644
index 0000000000000..44c72e555382f
--- /dev/null
+++ b/components/higher-order/with-dnd/index.js
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+import { DragDropContext, DragSource, DropTarget } from 'react-dnd';
+import MixedHTML5Backend from 'react-dnd-html5-mixed-backend';
+// import HTML5Backend from 'react-dnd-html5-backend';
+
+/**
+ * A wrapper around react-dnd DragDropContext higher order component.
+ * @param { Function } WrappedComponent Component to be wrapped with drag & drop context.
+ * @return { Function } A component wrapped with the react-dnd HOC.
+ */
+export default function withDragAndDropContext( WrappedComponent ) {
+ return DragDropContext( MixedHTML5Backend )( WrappedComponent );
+}
+
+/**
+ * @todo :clk:doc
+ * A wrapper around react-dnd DragSource higher order component.
+ * @param {[type]} itemType [description]
+ * @param {[type]} sourceSpec [description]
+ * @param {[type]} sourceCollect [description]
+ * @return {[type]} [description]
+ */
+export function withDragSource( itemType, sourceSpec, sourceCollect ) {
+ return ( WrappedComponent ) => {
+ return DragSource( itemType, sourceSpec, sourceCollect )( WrappedComponent );
+ };
+}
+
+/**
+ * @todo :clk:doc
+ * A wrapper around react-dnd DropTarget higher order component.
+ * @param {[type]} itemType [description]
+ * @param {[type]} targetSpec [description]
+ * @param {[type]} targetCollect [description]
+ * @return {[type]} [description]
+ */
+export function withDropTarget( itemType, targetSpec, targetCollect ) {
+ return ( WrappedComponent ) => {
+ return DropTarget( itemType, targetSpec, targetCollect )( WrappedComponent );
+ };
+}
diff --git a/components/higher-order/with-dnd/test/index.js b/components/higher-order/with-dnd/test/index.js
new file mode 100644
index 0000000000000..9b167f4254b6e
--- /dev/null
+++ b/components/higher-order/with-dnd/test/index.js
@@ -0,0 +1 @@
+// @todo :clk:tests
diff --git a/components/index.js b/components/index.js
index dc63a3eabae28..d86f2b27d36b0 100644
--- a/components/index.js
+++ b/components/index.js
@@ -45,3 +45,4 @@ export { default as withFocusReturn } from './higher-order/with-focus-return';
export { default as withInstanceId } from './higher-order/with-instance-id';
export { default as withSpokenMessages } from './higher-order/with-spoken-messages';
export { default as withState } from './higher-order/with-state';
+export { default as withDragAndDropContext, withDragSource, withDropTarget } from './higher-order/with-dnd';
diff --git a/editor/actions.js b/editor/actions.js
index 5a7733fc3cbf6..07c7a5bbe846e 100644
--- a/editor/actions.js
+++ b/editor/actions.js
@@ -177,6 +177,20 @@ export function replaceBlock( uid, block ) {
return replaceBlocks( uid, block );
}
+/**
+ * @todo :clk:doc
+ * @param {[type]} uid [description]
+ * @param {[type]} index [description]
+ * @return {[type]} [description]
+ */
+export function moveBlockToIndex( uid, index ) {
+ return {
+ type: 'MOVE_BLOCK_TO_INDEX',
+ uid,
+ index,
+ };
+}
+
export function insertBlock( block, position ) {
return insertBlocks( [ block ], position );
}
@@ -599,3 +613,25 @@ export function appendDefaultBlock() {
type: 'APPEND_DEFAULT_BLOCK',
};
}
+
+/**
+ * @todo :clk:doc
+ * [startReordering description]
+ * @return {[type]} [description]
+ */
+export function startReordering( ) {
+ return {
+ type: 'START_DRAG_AND_DROP',
+ };
+}
+
+/**
+ * @todo :clk:doc
+ * [stopReordering description]
+ * @return {[type]} [description]
+ */
+export function stopReordering( ) {
+ return {
+ type: 'STOP_DRAG_AND_DROP',
+ };
+}
diff --git a/editor/components/block-list/block.js b/editor/components/block-list/block.js
index 50fb5b47a2ae9..8ef8f89c3369b 100644
--- a/editor/components/block-list/block.js
+++ b/editor/components/block-list/block.js
@@ -24,6 +24,7 @@ import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
+import { DragAndDropSource, DragAndDropTarget } from '../draggable';
import BlockMover from '../block-mover';
import BlockDropZone from '../block-drop-zone';
import BlockSettingsMenu from '../block-settings-menu';
@@ -46,6 +47,7 @@ import {
stopTyping,
updateBlockAttributes,
toggleSelection,
+ moveBlockToIndex,
} from '../../actions';
import {
getBlock,
@@ -62,7 +64,9 @@ import {
isSelectionEnabled,
isTyping,
getBlockMode,
+ isReorderingInProgress,
} from '../../selectors';
+import { DRAGGABLE_BLOCK } from '../../constants';
const { BACKSPACE, ESCAPE, DELETE, ENTER, UP, RIGHT, DOWN, LEFT } = keycodes;
@@ -349,7 +353,7 @@ export class BlockListBlock extends Component {
}
render() {
- const { block, order, mode, showContextualToolbar, isLocked } = this.props;
+ const { block, order, mode, showContextualToolbar, isLocked, isReorderingInProgress } = this.props;
const { name: blockName, isValid } = block;
const blockType = getBlockType( blockName );
// translators: %s: Type of block (i.e. Text, Image etc)
@@ -397,7 +401,32 @@ export class BlockListBlock extends Component {
onClick={ this.onClick }
{ ...wrapperProps }
>
-
+
+
+ { true &&
+
+ DRAG
+
+ }
+
+ { true &&
+
+ DROP HERE
+
+ }
+
+ { ! this.props.isReorderingInProgress && }
+
{ ( showUI || isHovered ) && }
{ ( showUI || isHovered ) && }
{ showUI && isValid && showContextualToolbar && }
@@ -447,6 +476,7 @@ export class BlockListBlock extends Component {
{ !! error && }
+
);
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */
@@ -467,6 +497,7 @@ const mapStateToProps = ( state, { uid } ) => ( {
meta: getEditedPostAttribute( state, 'meta' ),
mode: getBlockMode( state, uid ),
isSelectionEnabled: isSelectionEnabled( state ),
+ isReorderingInProgress: isReorderingInProgress( state ),
} );
const mapDispatchToProps = ( dispatch, ownProps ) => ( {
@@ -477,6 +508,7 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( {
onSelect() {
dispatch( selectBlock( ownProps.uid ) );
},
+
onDeselect() {
dispatch( clearSelectedBlock() );
},
@@ -496,6 +528,7 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( {
uid: ownProps.uid,
} );
},
+
onMouseLeave() {
dispatch( {
type: 'TOGGLE_BLOCK_HOVERED',
@@ -527,9 +560,14 @@ const mapDispatchToProps = ( dispatch, ownProps ) => ( {
onMetaChange( meta ) {
dispatch( editPost( { meta } ) );
},
+
toggleSelection( selectionEnabled ) {
dispatch( toggleSelection( selectionEnabled ) );
},
+
+ onMoveBlockToIndex( uid, index ) {
+ dispatch( moveBlockToIndex( uid, index ) );
+ },
} );
BlockListBlock.className = 'editor-block-list__block-edit';
diff --git a/editor/components/block-list/index.js b/editor/components/block-list/index.js
index ba96280b555e1..e5b32b94691e0 100644
--- a/editor/components/block-list/index.js
+++ b/editor/components/block-list/index.js
@@ -17,8 +17,9 @@ import 'element-closest';
/**
* WordPress dependencies
*/
-import { Component } from '@wordpress/element';
+import { Component, compose } from '@wordpress/element';
import { serialize } from '@wordpress/blocks';
+import { withDragAndDropContext } from '@wordpress/components';
/**
* Internal dependencies
@@ -230,31 +231,34 @@ class BlockList extends Component {
}
}
-export default connect(
- ( state ) => ( {
- blocks: getBlockUids( state ),
- selectionStart: getMultiSelectedBlocksStartUid( state ),
- selectionEnd: getMultiSelectedBlocksEndUid( state ),
- multiSelectedBlocks: getMultiSelectedBlocks( state ),
- multiSelectedBlockUids: getMultiSelectedBlockUids( state ),
- selectedBlock: getSelectedBlock( state ),
- isSelectionEnabled: isSelectionEnabled( state ),
- } ),
- ( dispatch ) => ( {
- onStartMultiSelect() {
- dispatch( startMultiSelect() );
- },
- onStopMultiSelect() {
- dispatch( stopMultiSelect() );
- },
- onMultiSelect( start, end ) {
- dispatch( multiSelect( start, end ) );
- },
- onSelect( uid ) {
- dispatch( selectBlock( uid ) );
- },
- onRemove( uids ) {
- dispatch( { type: 'REMOVE_BLOCKS', uids } );
- },
- } )
+export default compose(
+ withDragAndDropContext,
+ connect(
+ ( state ) => ( {
+ blocks: getBlockUids( state ),
+ selectionStart: getMultiSelectedBlocksStartUid( state ),
+ selectionEnd: getMultiSelectedBlocksEndUid( state ),
+ multiSelectedBlocks: getMultiSelectedBlocks( state ),
+ multiSelectedBlockUids: getMultiSelectedBlockUids( state ),
+ selectedBlock: getSelectedBlock( state ),
+ isSelectionEnabled: isSelectionEnabled( state ),
+ } ),
+ ( dispatch ) => ( {
+ onStartMultiSelect() {
+ dispatch( startMultiSelect() );
+ },
+ onStopMultiSelect() {
+ dispatch( stopMultiSelect() );
+ },
+ onMultiSelect( start, end ) {
+ dispatch( multiSelect( start, end ) );
+ },
+ onSelect( uid ) {
+ dispatch( selectBlock( uid ) );
+ },
+ onRemove( uids ) {
+ dispatch( { type: 'REMOVE_BLOCKS', uids } );
+ },
+ } )
+ )
)( BlockList );
diff --git a/editor/components/draggable/drag-source.js b/editor/components/draggable/drag-source.js
new file mode 100644
index 0000000000000..b39354afd8ab4
--- /dev/null
+++ b/editor/components/draggable/drag-source.js
@@ -0,0 +1,87 @@
+/**
+ * External dependencies
+ */
+import { connect } from 'react-redux';
+import classnames from 'classnames';
+
+/**
+ * WordPress dependencies
+ */
+import { Component, compose } from '@wordpress/element';
+import { withDragSource } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+import { startReordering, stopReordering } from '../../actions';
+
+
+/**
+ * @todo :clk:doc
+ * Collector - properties to inject in the wrapped component
+ * @param {[type]} connect [description]
+ * @param {[type]} monitor [description]
+ * @return {[type]} [description]
+ */
+function sourceCollect( connect, monitor ) {
+ return {
+ connectDragSource: connect.dragSource(),
+ isDragging: monitor.isDragging()
+ };
+}
+
+/**
+ * @todo :clk:doc
+ * [sourceSpec description]
+ * @type {Object}
+ */
+const sourceSpec = {
+ beginDrag( props ) {
+ props.startReordering();
+
+ return {
+ dragSourceUid: props.dragSourceUid,
+ index: props.index,
+ };
+ },
+
+ // every drag is [guaranteed] to fire this endDrag callback
+ endDrag( props ) {
+ setTimeout( props.stopReordering, 500 );
+ // props.hideDropIndicator();
+ },
+
+};
+
+class DragSource extends Component {
+ constructor( props ) {
+ super( props );
+ }
+
+ render() {
+ const classes = classnames( 'draggable-drag-source');
+
+ return this.props.connectDragSource(
+
+ { this.props.children }
+
+ );
+ }
+}
+
+const mapDispatchToProps = ( dispatch ) => ( {
+ startReordering() {
+ dispatch( startReordering() );
+ },
+
+ stopReordering() {
+ dispatch( stopReordering() );
+ },
+
+} );
+
+export default compose(
+ connect( undefined, mapDispatchToProps ),
+ withDragSource( props => props.draggableType, sourceSpec, sourceCollect ),
+)( DragSource );
diff --git a/editor/components/draggable/drop-target.js b/editor/components/draggable/drop-target.js
new file mode 100644
index 0000000000000..42765140b7f57
--- /dev/null
+++ b/editor/components/draggable/drop-target.js
@@ -0,0 +1,124 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { findDOMNode } from 'react-dom';
+
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { withDropTarget } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import './style.scss';
+
+
+/**
+ * @todo :clk:doc
+ * [targetCollect description]
+ * @param {[type]} connect [description]
+ * @param {[type]} monitor [description]
+ * @return {[type]} [description]
+ */
+function targetCollect( connect, monitor ) {
+ return {
+ connectDropTarget: connect.dropTarget(),
+ isOver: monitor.isOver()
+ };
+}
+
+/**
+ * @todo :clk:doc
+ * [targetSpec description]
+ * @type {Object}
+ */
+const targetSpec = {
+ drop( props, monitor, component ) {
+ const dragIndex = monitor.getItem().index; // index of dragged item
+ const hoverIndex = props.index; // index of hovered over item
+
+ console.log(`Drag index: ${dragIndex}`);
+ console.log(`Drop index: ${hoverIndex}`);
+
+ if ( dragIndex === hoverIndex ) {
+ return;
+ }
+
+ props.reIndexCallback( monitor.getItem().dragSourceUid, hoverIndex );
+ return;
+
+ // monitor.getItem().index = hoverIndex;
+
+ // Determine rectangle on screen
+ const hoverBoundingRect = findDOMNode( component ).getBoundingClientRect();
+
+ // Get vertical middle
+ const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top ) / 2;
+
+ // Determine mouse position
+ const clientOffset = monitor.getClientOffset();
+
+ // Get pixels to the top
+ const hoverClientY = clientOffset.y - hoverBoundingRect.top;
+
+
+ // Dragging downwards
+ if ( dragIndex < hoverIndex ) {
+ let dropIndex;
+
+ if ( hoverClientY < hoverMiddleY ) {
+ dropIndex = hoverIndex - 1;
+ } else {
+ dropIndex = hoverIndex;
+ }
+
+ if ( dragIndex !== dropIndex ) {
+ // props.moveBlock( dragIndex, dropIndex );
+ props.reIndexCallback( monitor.getItem().dragSourceUid, dropIndex );
+ }
+ // monitor.getItem().index = dropIndex;
+ }
+
+ // Dragging upwards
+ if ( dragIndex > hoverIndex ) {
+ let dropIndex;
+
+ if ( hoverClientY > hoverMiddleY ) {
+ dropIndex = hoverIndex + 1;
+ } else {
+ dropIndex = hoverIndex;
+ }
+
+ if ( dragIndex !== dropIndex ) {
+ // props.moveBlock( dragIndex, dropIndex );
+ props.reIndexCallback( monitor.getItem().dragSourceUid, dropIndex );
+ }
+ // monitor.getItem().index = dropIndex;
+ }
+
+ },
+ hover( props, monitor, component ) {
+
+ }
+}
+
+class DropTarget extends Component {
+ constructor( props ) {
+ super( props );
+ }
+
+ render() {
+ const classes = classnames( 'draggable-drop-target');
+
+ return this.props.connectDropTarget(
+
+ { this.props.children }
+
+ );
+ }
+}
+
+export default withDropTarget( props => props.draggableType, targetSpec, targetCollect )( DropTarget );
diff --git a/editor/components/draggable/index.js b/editor/components/draggable/index.js
new file mode 100644
index 0000000000000..a89e3d96a3092
--- /dev/null
+++ b/editor/components/draggable/index.js
@@ -0,0 +1,2 @@
+export { default as DragAndDropSource } from './drag-source.js';
+export { default as DragAndDropTarget } from './drop-target.js';
diff --git a/editor/components/draggable/style.scss b/editor/components/draggable/style.scss
new file mode 100644
index 0000000000000..c6ca4c6fb235b
--- /dev/null
+++ b/editor/components/draggable/style.scss
@@ -0,0 +1,12 @@
+
+.draggable-drag-source {
+ border: 1px solid gray;
+ padding: 5px;
+ margin: 5px;
+}
+
+.draggable-drop-target {
+ border: 1px solid gray;
+ padding: 5px;
+ margin: 5px;
+}
diff --git a/editor/components/draggable/test/index.js b/editor/components/draggable/test/index.js
new file mode 100644
index 0000000000000..772624a40f685
--- /dev/null
+++ b/editor/components/draggable/test/index.js
@@ -0,0 +1 @@
+// @todo:clk:tests
diff --git a/editor/constants.js b/editor/constants.js
index 7d682b919c680..ec11afc9cd57e 100644
--- a/editor/constants.js
+++ b/editor/constants.js
@@ -9,3 +9,5 @@ export const BREAK_LARGE = parseInt( breakpointsScssVariables.breakLarge );
export const BREAK_MEDIUM = parseInt( breakpointsScssVariables.breakMedium );
export const BREAK_SMALL = parseInt( breakpointsScssVariables.breakSmall );
export const BREAK_MOBILE = parseInt( breakpointsScssVariables.breakMobile );
+export const DRAGGABLE_BLOCK = 'DRAGGABLE_BLOCK';
+export const DRAGGABLE_IMAGE = 'DRAGGABLE_IMAGE';
diff --git a/editor/reducer.js b/editor/reducer.js
index f7b02ff028e65..a239ce1f85cf9 100644
--- a/editor/reducer.js
+++ b/editor/reducer.js
@@ -236,6 +236,30 @@ export const editor = flow( [
];
}
+ case 'MOVE_BLOCK_TO_INDEX': {
+ if ( ! Number.isInteger( action.index ) || ! state.length ) {
+ return state;
+ }
+
+ const blockIndex = state.indexOf( action.uid );
+
+ if ( blockIndex === -1 || blockIndex === action.index ) {
+ return state;
+ }
+
+ if ( action.index < 0 ) {
+ action.index = 0;
+ } else if ( action.index >= state.length ) {
+ action.index = state.length - 1;
+ }
+
+ const _state = [ ...state ];
+
+ _state.splice( action.index, 0, _state.splice( blockIndex, 1 )[ 0 ] );
+
+ return _state;
+ }
+
case 'MOVE_BLOCKS_UP': {
const firstUid = first( action.uids );
const lastUid = last( action.uids );
@@ -782,6 +806,23 @@ export const reusableBlocks = combineReducers( {
},
} );
+/**
+ * @todo :clk:doc
+ * Reducer returning drag and drop in progress status.
+ * @return {Boolean} Updated state
+ */
+export function isReorderingInProgress( state = false, action ) {
+ switch ( action.type ) {
+ case 'START_DRAG_AND_DROP':
+ return true;
+
+ case 'STOP_DRAG_AND_DROP':
+ return false;
+ }
+
+ return state;
+}
+
export default optimist( combineReducers( {
editor,
currentPost,
@@ -797,4 +838,5 @@ export default optimist( combineReducers( {
metaBoxes,
browser,
reusableBlocks,
+ isReorderingInProgress,
} ) );
diff --git a/editor/selectors.js b/editor/selectors.js
index 565452ba2c184..2d1b53faabd4c 100644
--- a/editor/selectors.js
+++ b/editor/selectors.js
@@ -1147,3 +1147,12 @@ export function isPublishingPost( state ) {
// considered published
return !! stateBeforeRequest && ! isCurrentPostPublished( stateBeforeRequest );
}
+
+/**
+ * @todo :clk:doc
+ * @param {[type]} state [description]
+ * @return {Boolean} [description]
+ */
+export function isReorderingInProgress( state ) {
+ return state.isReorderingInProgress;
+}
diff --git a/editor/test/reducer.js b/editor/test/reducer.js
index 08426a96d4d4f..8da7ca12247cd 100644
--- a/editor/test/reducer.js
+++ b/editor/test/reducer.js
@@ -403,6 +403,84 @@ describe( 'state', () => {
expect( state.present.blockOrder ).toEqual( [ 'kumquat', 'persimmon', 'loquat' ] );
} );
+ it( 'should move block to lower index', () => {
+ const original = editor( undefined, {
+ type: 'RESET_BLOCKS',
+ blocks: [ {
+ uid: 'chicken',
+ name: 'core/test-block',
+ attributes: {},
+ }, {
+ uid: 'ribs',
+ name: 'core/test-block',
+ attributes: {},
+ }, {
+ uid: 'veggies',
+ name: 'core/test-block',
+ attributes: {},
+ } ],
+ } );
+ const state = editor( original, {
+ type: 'MOVE_BLOCK_TO_INDEX',
+ uid: 'ribs',
+ index: 0
+ } );
+
+ expect( state.present.blockOrder ).toEqual( [ 'ribs', 'chicken', 'veggies', ] );
+ } );
+
+ it( 'should move block to higher index', () => {
+ const original = editor( undefined, {
+ type: 'RESET_BLOCKS',
+ blocks: [ {
+ uid: 'chicken',
+ name: 'core/test-block',
+ attributes: {},
+ }, {
+ uid: 'ribs',
+ name: 'core/test-block',
+ attributes: {},
+ }, {
+ uid: 'veggies',
+ name: 'core/test-block',
+ attributes: {},
+ } ],
+ } );
+ const state = editor( original, {
+ type: 'MOVE_BLOCK_TO_INDEX',
+ uid: 'ribs',
+ index: 2
+ } );
+
+ expect( state.present.blockOrder ).toEqual( [ 'chicken', 'veggies', 'ribs', ] );
+ } );
+
+ it( 'should not move block if passed same index', () => {
+ const original = editor( undefined, {
+ type: 'RESET_BLOCKS',
+ blocks: [ {
+ uid: 'chicken',
+ name: 'core/test-block',
+ attributes: {},
+ }, {
+ uid: 'ribs',
+ name: 'core/test-block',
+ attributes: {},
+ }, {
+ uid: 'veggies',
+ name: 'core/test-block',
+ attributes: {},
+ } ],
+ } );
+ const state = editor( original, {
+ type: 'MOVE_BLOCK_TO_INDEX',
+ uid: 'ribs',
+ index: 1
+ } );
+
+ expect( state.present.blockOrder ).toEqual( [ 'chicken', 'ribs', 'veggies', ] );
+ } );
+
describe( 'edits()', () => {
it( 'should save newly edited properties', () => {
const original = editor( undefined, {
diff --git a/package-lock.json b/package-lock.json
index 0125e8325c709..55d2a9d0e2b6b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2800,6 +2800,22 @@
"integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=",
"dev": true
},
+ "disposables": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/disposables/-/disposables-1.0.1.tgz",
+ "integrity": "sha1-BkcnoltU9QK9griaot+4358bOeM="
+ },
+ "dnd-core": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-2.5.4.tgz",
+ "integrity": "sha512-BcI782MfTm3wCxeIS5c7tAutyTwEIANtuu3W6/xkoJRwiqhRXKX3BbGlycUxxyzMsKdvvoavxgrC3EMPFNYL9A==",
+ "requires": {
+ "asap": "2.0.6",
+ "invariant": "2.2.2",
+ "lodash": "4.17.4",
+ "redux": "3.7.2"
+ }
+ },
"doctrine": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz",
@@ -9297,6 +9313,49 @@
}
}
},
+ "react-dnd": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-2.5.4.tgz",
+ "integrity": "sha512-y9YmnusURc+3KPgvhYKvZ9oCucj51MSZWODyaeV0KFU0cquzA7dCD1g/OIYUKtNoZ+MXtacDngkdud2TklMSjw==",
+ "requires": {
+ "disposables": "1.0.1",
+ "dnd-core": "2.5.4",
+ "hoist-non-react-statics": "2.3.1",
+ "invariant": "2.2.2",
+ "lodash": "4.17.4",
+ "prop-types": "15.5.10"
+ },
+ "dependencies": {
+ "hoist-non-react-statics": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz",
+ "integrity": "sha1-ND24TGAYxlB3iJgkATWhQg7iLOA="
+ }
+ }
+ },
+ "react-dnd-html5-backend": {
+ "version": "2.5.4",
+ "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.4.tgz",
+ "integrity": "sha512-jDqAkm/hI8Tl4HcsbhkBgB6HgpJR1e+ML1SbfxaegXYiuMxEVQm0FOwEH5WxUoo6fmIG4N+H0rSm59POuZOCaA==",
+ "requires": {
+ "lodash": "4.17.4"
+ }
+ },
+ "react-dnd-html5-mixed-backend": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/react-dnd-html5-mixed-backend/-/react-dnd-html5-mixed-backend-2.0.1.tgz",
+ "integrity": "sha1-z1oFCA8V6b/qA+iKwP4yb5WNDjE=",
+ "requires": {
+ "lodash": "3.10.1"
+ },
+ "dependencies": {
+ "lodash": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
+ "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
+ }
+ }
+ },
"react-dom": {
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.2.0.tgz",
diff --git a/package.json b/package.json
index 58eb251a480e8..0895d0e6b2c8c 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,9 @@
"react-click-outside": "2.3.1",
"react-color": "2.13.4",
"react-datepicker": "0.61.0",
+ "react-dnd": "2.5.4",
+ "react-dnd-html5-backend": "2.5.4",
+ "react-dnd-html5-mixed-backend": "2.0.1",
"react-dom": "16.2.0",
"react-redux": "5.0.6",
"redux": "3.7.2",