react-components
Version:
React components used by Khan Academy
178 lines (164 loc) • 5.79 kB
JSX
const React = require('react');
const ReactDOM = require("react-dom");
const PT = React.PropTypes;
// Takes an array of components to sort
const SortableArea = React.createClass({
propTypes: {
className: PT.string,
components: PT.arrayOf(PT.node).isRequired,
onReorder: PT.func.isRequired,
style: PT.any,
verify: PT.func,
},
getDefaultProps: function() {
return {verify: () => true};
},
getInitialState: function() {
return {
// index of the component being dragged
dragging: null,
components: this.props.components,
};
},
// Firefox refuses to drag an element unless you set data on it. Hackily
// add data each time an item is dragged.
componentDidMount: function() {
this._setDragEvents();
},
componentWillReceiveProps: function(nextProps) {
this.setState({components: nextProps.components});
},
componentDidUpdate: function() {
this._setDragEvents();
},
// Alternatively send each handler to each component individually,
// partially applied
onDragStart: function(startIndex) {
this.setState({dragging: startIndex});
},
onDrop: function() {
// tell the parent component
this.setState({dragging: null});
this.props.onReorder(this.state.components);
},
onDragEnter: function(enterIndex) {
// When a label is first dragged it triggers a dragEnter with itself,
// which we don't care about.
if (this.state.dragging === enterIndex) {
return;
}
const newComponents = this.state.components.slice();
// splice the tab out of its old position
const removed = newComponents.splice(this.state.dragging, 1);
// ... and into its new position
newComponents.splice(enterIndex, 0, removed[0]);
const verified = this.props.verify(newComponents);
if (verified) {
this.setState({
dragging: enterIndex,
components: newComponents,
});
}
return verified;
},
_listenEvent: function(e) {
e.dataTransfer.setData('hackhackhack', 'because browsers!');
},
_cancelEvent: function(e) {
// prevent the browser from redirecting to 'because browsers!'
e.preventDefault();
},
_setDragEvents: function() {
this._dragItems = this._dragItems || [];
const items = ReactDOM.findDOMNode(this)
.querySelectorAll('[draggable=true]');
const oldItems = [];
const newItems = [];
for (let i = 0; i < this._dragItems.length; i++) {
const item = this._dragItems[i];
if (items.indexOf(item) < 0) {
oldItems.push(item);
}
}
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (this._dragItems.indexOf(item) < 0) {
newItems.push(item);
}
}
for (let i = 0; i < newItems.length; i++) {
const dragItem = newItems[i];
dragItem.addEventListener('dragstart', this._listenEvent);
dragItem.addEventListener('drop', this._cancelEvent);
}
for (let i = 0; i < oldItems.length; i++) {
const dragItem = oldItems[i];
dragItem.removeEventListener('dragstart', this._listenEvent);
dragItem.removeEventListener('drop', this._cancelEvent);
}
},
render: function() {
const sortables = this.state.components.map((component, index) =>
<SortableItem
index={index}
component={component}
area={this}
key={component.key}
draggable={component.props.draggable}
dragging={index === this.state.dragging}
/>
);
return <ol className={this.props.className} style={this.props.style}>
{sortables}
</ol>;
},
});
// An individual sortable item
const SortableItem = React.createClass({
propTypes: {
area: PT.shape({
onDragEnter: PT.func.isRequired,
onDragStart: PT.func.isRequired,
onDrop: PT.func.isRequired,
}),
component: PT.node.isRequired,
dragging: PT.bool.isRequired,
draggable: PT.bool.isRequired,
index: PT.number.isRequired,
},
handleDragStart: function(e) {
e.nativeEvent.dataTransfer.effectAllowed = "move";
this.props.area.onDragStart(this.props.index);
},
handleDrop: function() {
this.props.area.onDrop(this.props.index);
},
handleDragEnter: function(e) {
const verified = this.props.area.onDragEnter(this.props.index);
// Ideally this would change the cursor based on whether this is a
// valid place to drop.
e.nativeEvent.dataTransfer.effectAllowed = verified ? "move" : "none";
},
handleDragOver: function(e) {
// allow a drop by preventing default handling
e.preventDefault();
},
render: function() {
let dragState = "sortable-disabled";
if (this.props.dragging) {
dragState = "sortable-dragging";
} else if (this.props.draggable) {
dragState = "sortable-enabled";
}
return <li draggable={this.props.draggable}
className={dragState}
onDragStart={this.handleDragStart}
onDrop={this.handleDrop}
onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver}
>
{this.props.component}
</li>;
},
});
module.exports = SortableArea;