diff --git a/packages/component-sortable-list/package.json b/packages/component-sortable-list/package.json new file mode 100644 index 0000000000000000000000000000000000000000..3ff0d8c6be2c0d7dd64296fe7e9c374ab9ff9498 --- /dev/null +++ b/packages/component-sortable-list/package.json @@ -0,0 +1,12 @@ +{ + "name": "pubsweet-component-sortable-list", + "version": "0.0.1", + "main": "src", + "license": "MIT", + "dependencies": { + "react": "^15.6.1", + "react-dnd": "^2.5.4", + "react-dom": "^15.6.1", + "recompose": "^0.26.0" + } +} diff --git a/packages/component-sortable-list/src/components/SortableList.js b/packages/component-sortable-list/src/components/SortableList.js new file mode 100644 index 0000000000000000000000000000000000000000..e454020635d88c8712efb97209a30001803a3227 --- /dev/null +++ b/packages/component-sortable-list/src/components/SortableList.js @@ -0,0 +1,133 @@ +import React from 'react' +import { pick } from 'lodash' +import { compose } from 'recompose' +import { findDOMNode } from 'react-dom' +import { DragSource, DropTarget } from 'react-dnd' + +const itemSource = { + beginDrag(props) { + return pick(props, props.beginDragProps) + }, +} + +const itemTarget = { + hover({ moveItem, index, listId }, monitor, component) { + const { index: dragIndex, listId: toListId } = monitor.getItem() + const hoverIndex = index + + if (listId !== toListId) { + return + } + + // Don't replace items with themselves + if (dragIndex === hoverIndex) { + return + } + + const hoverBoundingRect = findDOMNode(component).getBoundingClientRect() // eslint-disable-line + const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2 + const clientOffset = monitor.getClientOffset() + const hoverClientY = clientOffset.y - hoverBoundingRect.top + + if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) { + return + } + + if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) { + return + } + if (typeof moveItem === 'function') { + moveItem(dragIndex, hoverIndex) + } + monitor.getItem().index = hoverIndex + }, + drop({ dropItem, ...restProps }, monitor) { + if (dropItem && typeof dropItem === 'function') + dropItem(restProps, monitor.getItem()) + }, +} + +const Item = ({ + connectDragPreview, + connectDragSource, + connectDropTarget, + listItem, + dragHandle, + ...rest +}) => + dragHandle + ? connectDragPreview( + connectDropTarget( + <div style={{ flex: 1 }}> + {React.createElement(listItem, { + ...rest, + dragHandle: connectDragSource( + <div style={{ display: 'flex' }}> + {React.createElement(dragHandle)} + </div>, + ), + })} + </div>, + ), + ) + : connectDropTarget( + connectDragSource( + <div style={{ flex: 1 }}>{React.createElement(listItem, rest)}</div>, + ), + ) + +const DecoratedItem = compose( + DropTarget('item', itemTarget, (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + })), + DragSource('item', itemSource, (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + connectDragPreview: connect.dragPreview(), + isDragging: monitor.isDragging(), + })), +)(Item) + +const SortableList = ({ + items, + itemKey = 'id', + moveItem, + listItem, + dragHandle, + editItem, + ...rest +}) => ( + <div> + {items.map((item, i) => ( + <DecoratedItem + dragHandle={dragHandle} + index={i} + key={item[itemKey]} + listItem={listItem} + moveItem={moveItem} + {...item} + {...rest} + /> + ))} + </div> +) + +// helper function for sortable lists +SortableList.moveItem = (items, dragIndex, hoverIndex) => { + if (dragIndex <= hoverIndex) { + return [ + ...items.slice(0, dragIndex), + items[hoverIndex], + items[dragIndex], + ...items.slice(hoverIndex + 1), + ] + } + return [ + ...items.slice(0, hoverIndex), + items[dragIndex], + items[hoverIndex], + ...items.slice(dragIndex + 1), + ] +} + +export default SortableList diff --git a/packages/component-sortable-list/src/components/SortableList.md b/packages/component-sortable-list/src/components/SortableList.md new file mode 100644 index 0000000000000000000000000000000000000000..041ee606fb2ded7ba45bd5597431835db99da112 --- /dev/null +++ b/packages/component-sortable-list/src/components/SortableList.md @@ -0,0 +1,115 @@ +A sortable list implemented with `react-dnd`. + +## Props + +| Prop | Description | Required | Default | Type | +| :------------: | :------------------------------------------------------------------------------------------------------------------------------------------: | :------: | :-----: | :-------------: | +| items | The items of the sortable list. | true | [] | Array | +| itemKey | Value used for key when mapping over items. | true | 'id' | string | +| listItem | A React component that will be rendered for each item of the list. Receives `isDragging`, `isOver` and all other props from the items array. | true | none | React component | +| moveItem | Function to be called when moving an item through the list. SortableList will provide the dragIndex of hoverIndex of the items. | true | none | function | +| dragHandle | A React component for the drag handle. If not present, the whole item can be dragged. | false | none | React component | +| dropItem | Function to be called when dropping an item. The index of the dragged item is passed. | false | none | function | +| beginDragProps | Array of keys to pick from the dragged object when beginning drag. | false | [] | Array(string) | + +## Usage + +This component should be used in a React-DnD `DragDropContext` or `DragDropContextProvider`. Make sure you have `react-dnd-html5-backend` installed and wrap the parent component with `DragDropContext` decorator or add the `DragDropContextProvider` in your root component. + +```js +import HTML5Backend from 'react-dnd-html5-backend' +import { DragDropContext } from 'react-dnd' + +class YourApp { + /* ... */ +} + +export default DragDropContext(HTML5Backend)(YourApp) +``` + +or + +```js +import HTML5Backend from 'react-dnd-html5-backend' +import { DragDropContextProvider } from 'react-dnd' + +export default class YourApp { + render() { + return ( + <DragDropContextProvider backend={HTML5Backend}> + /* ... */ + </DragDropContextProvider> + ) + } +} +``` + +### Pass in a list of users + +```js +const items = [ + {firstName: 'John', lastName: 'Doe'}, + {firstName: 'Michael', lastName: 'Jackson'}, + {firstName: 'David', lastName: 'Blaine'}, +] + +const Item = ({ isOver, isDragging, ...rest }) => + <div>`${rest.firstName} ${rest.lastName}`</div> + +<SortableList + items={items} + listItem={Item} + moveItem={(dragIndex, hoverIndex) => change items} + /> +``` + +### With custom drag handle + +```js +const DragHandle = () => <div>Drag me!</div> + +const ItemWithDragHandle = ({ dragHandle, ...rest }) => <div> + {dragHandle} + <span>Rest of the content.</span> + </div> + +<SortableList + ... + listItem={ItemWithDragHandle} + dragHandle={DragHandle} + ... + /> +``` + +### How to move items around + +To move items of the parent container whenever `moveItem` function is called we can use the `SortableList.moveItem` helper. More info in the example below. + +```js +const Container = ({ moveItem, items }) => ( + <div> + ... + <SortableList items={items} listItem={Item} moveItem={moveItem} /> + ... + </div> +) +``` + +Enhanced using recompose + +```js +const MoveExample = compose( + withState('items', 'setItems', [ + { name: 'John' }, + { name: 'Nancy' }, + { name: 'Adam' }, + ]), + withHandlers({ + moveItem: ({ setItems, items }) => (dragIndex, hoverIndex) => { + setItems(prevItems => + SortableList.moveItem(prevItems, dragIndex, hoverIndex), + ) + }, + }), +)(Container) +``` diff --git a/packages/component-sortable-list/src/components/index.js b/packages/component-sortable-list/src/components/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5468a1bf42a9d588ee1f770d1d7c1b917ed17ca5 --- /dev/null +++ b/packages/component-sortable-list/src/components/index.js @@ -0,0 +1 @@ +export { default as SortableList } from './SortableList' diff --git a/packages/component-sortable-list/src/index.js b/packages/component-sortable-list/src/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8d52f148e8c8927db1d86494ca404451da5a917d --- /dev/null +++ b/packages/component-sortable-list/src/index.js @@ -0,0 +1,5 @@ +module.exports = { + client: { + components: [() => require('./components')], + }, +} diff --git a/packages/components-faraday/src/components/AuthorList/AuthorList.js b/packages/components-faraday/src/components/AuthorList/AuthorList.js index 382931818f802c5cc66b6d90e4f256f2e1d5a13e..9b00dd834522826565e79cd7d4a8a24f17e49b84 100644 --- a/packages/components-faraday/src/components/AuthorList/AuthorList.js +++ b/packages/components-faraday/src/components/AuthorList/AuthorList.js @@ -11,7 +11,7 @@ import { withState, } from 'recompose' import { change as changeForm } from 'redux-form' -import { SortableList } from 'pubsweet-components-faraday/src/components' +import { SortableList } from 'pubsweet-component-sortable-list/src/components' import { addAuthor } from '../../redux/authors'