mirror of
https://github.com/zoriya/flood.git
synced 2026-05-31 18:25:25 +00:00
Add sortable components
This commit is contained in:
@@ -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));
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user