Add sortable components

This commit is contained in:
John Furrow
2017-02-25 18:42:30 -08:00
parent 5748670fd9
commit 1a175db089
5 changed files with 424 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
$sortable-list--item--background: saturate(darken($modal--background, 1%), 3%);
$sortable-list--item--background--hover: saturate(darken($modal--background, 2%), 3%);
$sortable-list--item--background--dragging: desaturate(lighten($modal--background, 2%), 1%);
$sortable-list--item--background--preview: saturate(darken($modal--background, 3%), 3%);
$sortable-list--item--border: saturate(darken($modal--background, 4%), 5%);
$sortable-list--item--border--preview: darken($sortable-list--item--border, 3%);
.sortable-list {
font-size: 0.9em;
position: relative;
&__item {
align-items: center;
background: $sortable-list--item--background;
border: 1px solid $sortable-list--item--border;
cursor: move;
display: flex;
height: 30px;
padding: 0 $spacing-unit * 1/5;
&:first-child {
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
&:last-child {
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
}
&--is-dragging {
background: $sortable-list--item--background--dragging;
opacity: 0.6;
position: relative;
z-index: 1;
label {
opacity: 0.25;
}
}
&--is-locked {
cursor: default;
opacity: 0.4;
}
&--is-preview {
background: $sortable-list--item--background--preview;
border: 1px solid $sortable-list--item--border--preview;
border-radius: 0;
color: $white;
font-weight: 500;
left: 0;
position: absolute;
top: 0;
width: 100%;
}
& + .sortable-list {
&__item {
margin-top: -1px;
}
}
.icon {
fill: currentColor;
height: 12px;
width: 12px;
&--error {
fill: $red;
}
}
}
&__content {
flex: 0 0 auto;
margin-left: $spacing-unit * 1/5;
&:first-child {
margin-left: 0;
}
&__wrapper {
align-items: center;
display: flex;
flex: 1 1 auto;
}
&--primary {
flex: 1 1 auto;
}
&--secondary {
align-items: center;
font-size: 0.9em;
&,
&.tooltip__wrapper {
display: flex;
}
&__copy {
opacity: 0.5;
}
}
}
}
@@ -0,0 +1,105 @@
import classnames from 'classnames';
import {DragDropContext} from 'react-dnd';
import {FormattedMessage, injectIntl} from 'react-intl';
import HTML5Backend from 'react-dnd-html5-backend';
import React from 'react';
import SortableListItemDragLayer from './SortableListItemDragLayer';
import Checkbox from './FormElements/Checkbox';
import SortableListItem from './SortableListItem';
import TorrentProperties from '../../constants/TorrentProperties';
const methodsToBind = ['handleDrop', 'handleMove', 'handleMouseDown'];
class SortableList extends React.Component {
constructor(props) {
super(props);
this.sortableListRef = null;
this.state = {
listOffset: null,
items: props.items
};
methodsToBind.forEach(method => this[method] = this[method].bind(this));
}
componentWillReceiveProps(nextProps) {
this.setState({items: nextProps.items});
}
handleDrop() {
if (this.props.onDrop) {
this.props.onDrop(this.state.items);
}
}
handleMouseDown(event) {
if (this.sortableListRef != null) {
this.setState({
listOffset: this.sortableListRef.getBoundingClientRect()
});
}
if (this.props.onMouseDown) {
this.props.onMouseDown(event);
}
}
handleMove(dragIndex, hoverIndex) {
const {items} = this.state;
const draggedItem = items[dragIndex];
// Remove the item being dragged.
items.splice(dragIndex, 1);
// Add the item being dragged in its new position.
items.splice(hoverIndex, 0, draggedItem);
this.setState({items});
if (this.props.onMove) {
this.props.onMove(items);
}
}
getItemList() {
const {
handleDrop,
handleMove,
state: {items},
props: {lockedIDs, renderItem}
} = this;
return items.map((item, index) => {
const {id, visible} = item;
return (
<SortableListItem id={id}
index={index}
isLocked={lockedIDs.includes(id)}
isVisible={visible}
key={id}
onDrop={handleDrop}
onMove={handleMove}>
{renderItem(item, index)}
</SortableListItem>
);
});
}
render() {
const classes = classnames('sortable-list', this.props.className);
return (
<ul className={classes}
onMouseDown={this.handleMouseDown}
ref={ref => this.sortableListRef = ref}>
<SortableListItemDragLayer items={this.state.items}
listOffset={this.state.listOffset} />
{this.getItemList()}
</ul>
);
}
}
export default DragDropContext(HTML5Backend)(injectIntl(SortableList));
@@ -0,0 +1,122 @@
import _ from 'lodash';
import classnames from 'classnames';
import {DragSource, DropTarget} from 'react-dnd';
import {getEmptyImage} from 'react-dnd-html5-backend';
import React from 'react';
import ReactDOM from 'react-dom';
import LockIcon from '../Icons/LockIcon';
const itemSource = {
beginDrag({id, index, isVisible}) {
return {id, index, isVisible};
},
canDrag({isLocked}) {
return !isLocked;
}
};
const itemTarget = {
drop(props, monitor, component) {
if (props.onDrop) {
props.onDrop();
}
},
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index;
const {index: hoverIndex, isVisible, isLocked} = props;
// Don't replace items with themselves
if (isLocked || dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = ReactDOM.findDOMNode(component)
.getBoundingClientRect();
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get the remaining pixels to the top of the list.
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
const isDraggingUp = dragIndex < hoverIndex;
const isDraggingDown = dragIndex > hoverIndex;
const dragThreshhold = isDraggingDown
? hoverBoundingRect.height * 0.85
: hoverBoundingRect.height * 0.15;
// Return early if we haven't dragged more than halfway past the next item.
if ((isDraggingUp && hoverClientY < dragThreshhold)
|| (isDraggingDown && hoverClientY > dragThreshhold)) {
return;
}
props.onMove(dragIndex, hoverIndex);
monitor.getItem().index = hoverIndex;
}
};
class SortableListItem extends React.Component {
componentDidMount() {
// Replace the native drag preview with an empty image.
this.props.connectDragPreview(getEmptyImage(), {
captureDraggingState: true
});
}
render() {
const {
children,
isDragging,
isLocked,
connectDragSource,
connectDropTarget
} = this.props;
let lockedIcon = null;
if (isLocked) {
lockedIcon = <LockIcon />;
}
const classes = classnames('sortable-list__item', {
'sortable-list__item--is-dragging': isDragging,
'sortable-list__item--is-locked': isLocked
});
return connectDragSource(
connectDropTarget(
(
<div className={classes}>
{lockedIcon}
{children}
</div>
)
)
);
}
}
SortableListItem.propTypes = {
id: React.PropTypes.string
};
export default _.flow([
DragSource('globally-draggable-item', itemSource, (connect, monitor) => {
return {
connectDragPreview: connect.dragPreview(),
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
};
}),
DropTarget('globally-draggable-item', itemTarget, connect => {
return {
connectDropTarget: connect.dropTarget()
};
})
])(SortableListItem);
@@ -0,0 +1,86 @@
import {DragLayer} from 'react-dnd';
import {FormattedMessage, injectIntl} from 'react-intl';
import React, {Component, PropTypes} from 'react';
import Checkbox from '../../components/General/FormElements/Checkbox';
import TorrentProperties from '../../constants/TorrentProperties';
const layerStyles = {
position: 'absolute',
pointerEvents: 'none',
zIndex: 100,
left: 0,
top: 0,
width: '100%',
height: '100%',
};
function getItemStyles(props) {
const {clientOffset, differenceFromInitialOffset, listOffset} = props;
if (!clientOffset || !listOffset) {
return {display: 'none'};
}
const x = differenceFromInitialOffset.x;
const y = clientOffset.y - listOffset.top - 15;
return {transform: `translate(${x}px, ${y}px)`};
}
class SortableListItemDragLayer extends Component {
renderItem(type, item) {
switch (type) {
case 'globally-draggable-item':
return (
<div className="sortable-list__item sortable-list__item--is-preview">
<div className="sortable-list__content__wrapper">
<span className="sortable-list__content sortable-list__content--primary">
<FormattedMessage id={TorrentProperties[item.id].id}
defaultMessage={TorrentProperties[item.id].defaultMessage} />
</span>
<span className="sortable-list__content sortable-list__content--secondary">
<Checkbox checked={item.isVisible}>
Enabled
</Checkbox>
</span>
</div>
</div>
);
default:
return null;
}
}
render() {
const {item, itemType, isDragging} = this.props;
if (!isDragging) {
return null;
}
return (
<div style={layerStyles}>
<div style={getItemStyles(this.props)}>
{this.renderItem(itemType, item)}
</div>
</div>
);
}
}
SortableListItemDragLayer.propTypes = {
clientOffset: PropTypes.object,
differenceFromInitialOffset: PropTypes.object,
isDragging: PropTypes.bool.isRequired,
item: PropTypes.object,
itemType: PropTypes.string
};
export default DragLayer(monitor => ({
clientOffset: monitor.getClientOffset(),
differenceFromInitialOffset: monitor.getDifferenceFromInitialOffset(),
isDragging: monitor.isDragging(),
item: monitor.getItem(),
itemType: monitor.getItemType()
}))(injectIntl(SortableListItemDragLayer));
+2
View File
@@ -83,6 +83,8 @@
"react-addons-create-fragment": "^15.0.2",
"react-addons-css-transition-group": "^15.0.2",
"react-custom-scrollbars": "^4.0.0",
"react-dnd": "^2.2.3",
"react-dnd-html5-backend": "^2.2.3",
"react-dom": "^15.0.2",
"react-dropzone": "^3.4.0",
"react-intl": "^2.1.3",