Skip to content

Instantly share code, notes, and snippets.

@WuTheFWasThat
Created July 30, 2016 04:12
Show Gist options
  • Save WuTheFWasThat/a42d9fa321b8c412b927d5fbd03a1b6a to your computer and use it in GitHub Desktop.
Save WuTheFWasThat/a42d9fa321b8c412b927d5fbd03a1b6a to your computer and use it in GitHub Desktop.
Sortable: Drag-to-reorder list
// I wrote this for work today after being unable to find a sortable list API for react to my liking
// Beware: It's only been tested for my use case
// Based largely off of https://github.com/danielstocks/react-sortable/blob/master/src/SortableComposition.js
export default class Sortable extends React.Component {
static get propTypes() {
return React.PropTypes.shape({
items: React.PropTypes.array.isRequired,
updateState: React.PropTypes.func.isRequired,
// template should respect *inherits*
template: React.PropTypes.func.isRequired,
// sortId: React.PropTypes.number,
outline: React.PropTypes.string.isRequired, // row | column
}).isRequired;
}
constructor(props) {
super(props);
this.state = {
items: this.props.items.slice(),
draggingIndex : null,
elementEdge: 0,
updateEdge: true
};
}
componentWillReceiveProps(props) {
this.setState({
items: props.items.slice(),
});
}
sortEnd() {
this.props.updateState({
items: this.state.items
});
this.setState({
draggingIndex: null
});
}
sortStart(e) {
const draggingIndex = e.currentTarget.dataset.id;
this.setState({
draggingIndex: draggingIndex,
updateEdge: true
});
if (e.dataTransfer !== undefined) {
e.dataTransfer.setData('text', e.target);
}
}
dragOver(e) {
e.preventDefault();
let mouseBeyond;
let positionX, positionY;
let topOffset;
let items = this.state.items;
//underlying element //TODO: not working for touch
const overEl = e.currentTarget;
//index of underlying element in the set DOM elements
const indexDragged = Number(overEl.dataset.id);
const indexFrom = Number(this.state.draggingIndex);
const height = overEl.getBoundingClientRect().height;
if (e.type === 'dragover') {
positionX = e.clientX;
positionY = e.clientY;
topOffset = overEl.offsetTop - overEl.scrollTop + overEl.clientTop;
} else if (e.type === 'touchmove') {
positionX = e.touches[0].pageX;
positionY = e.touches[0].pageY;
let elementEdge = this.state.elementEdge;
if (this.state.updateEdge) {
elementEdge = e.currentTarget.getBoundingClientRect().top;
this.setState({
updateEdge: false,
elementEdge: elementEdge
});
}
e.currentTarget.style.top = (positionY - elementEdge) + 'px';
topOffset = elementEdge;
}
function isMouseBeyond(mousePos, elementPos, elementSize) {
// break point is set to the middle line of element
const breakPoint = elementSize / 2;
const mouseOverlap = mousePos - elementPos;
return mouseOverlap > breakPoint;
}
if (this.props.outline === 'list') {
mouseBeyond = isMouseBeyond(positionY, topOffset, height);
} else if (this.props.outline === 'column') {
mouseBeyond = isMouseBeyond(
positionX,
overEl.getBoundingClientRect().left,
overEl.getBoundingClientRect().width
);
}
if (indexDragged !== indexFrom && mouseBeyond) {
const newItems = items.slice();
const item = newItems.splice(indexFrom, 1)[0];
newItems.splice(indexDragged, 0, item);
this.setState({
items: newItems,
draggingIndex: indexDragged
});
}
}
render() {
return (
<ul>
{
this.state.items.map((item, i) => {
return this.props.template({
item: item,
dragging: (i === this.props.draggingIndex),
dragProps: {
'draggable' : true,
'onDragOver' : this.dragOver.bind(this),
'onDragStart' : this.sortStart.bind(this),
'onDragEnd' : this.sortEnd.bind(this),
'onTouchStart' : this.sortStart.bind(this),
'onTouchMove' : this.dragOver.bind(this),
'onTouchEnd' : this.sortEnd.bind(this),
'data-id' : i,
}
});
})
}
</ul>
);
}
}
@WuTheFWasThat
Copy link
Author

WuTheFWasThat commented Jul 30, 2016

example usage (untested):

      <Sortable
        items={[{id: 1, name: 'Alice'}, {id: 7, name: 'Eve'}, {id: 13, name: 'Bob'}]}
        template={(info) => {
          const person = info.item;
          return (
            <div key={person.id}>
              <span className="reorder-handle" {...info.dragProps}>
                <i className='fa fa-bars'></i>
              </span>
              <span>{person.name}</span>
            </div>
          );
        }}
        outline="list"
        updateState={(result) => { 
          // do stuff with result.items, which is reordered
        }}
      />

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment