react-components
Version:
React components used by Khan Academy
174 lines (160 loc) • 5.74 kB
JavaScript
const React = require('react');
const ReactDOM = require("react-dom");
const PT = React.PropTypes;
// Takes an array of components to sort
const SortableArea = React.createClass({displayName: "SortableArea",
propTypes: {
className: PT.string,
components: PT.arrayOf(PT.node).isRequired,
onReorder: PT.func.isRequired,
style: PT.any,
verify: PT.func,
},
getDefaultProps: function() {
return {verify: function() {return 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 (const item of this._dragItems) {
if (items.indexOf(item) < 0) {
oldItems.push(item);
}
}
for (const item of items) {
if (this._dragItems.indexOf(item) < 0) {
oldItems.push(item);
}
}
for (const dragItem of newItems) {
dragItem.addEventListener('dragstart', this._listenEvent);
dragItem.addEventListener('drop', this._cancelEvent);
}
for (const dragItem of oldItems) {
dragItem.removeEventListener('dragstart', this._listenEvent);
dragItem.removeEventListener('drop', this._cancelEvent);
}
},
render: function() {
const sortables = this.state.components.map(function(component, index)
{return React.createElement(SortableItem, {
index: index,
component: component,
area: this,
key: component.key,
draggable: component.props.draggable,
dragging: index === this.state.dragging}
);}.bind(this)
);
return React.createElement("ol", {className: this.props.className, style: this.props.style},
sortables
);
},
});
// An individual sortable item
const SortableItem = React.createClass({displayName: "SortableItem",
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 React.createElement("li", {draggable: this.props.draggable,
className: dragState,
onDragStart: this.handleDragStart,
onDrop: this.handleDrop,
onDragEnter: this.handleDragEnter,
onDragOver: this.handleDragOver
},
this.props.component
);
},
});
module.exports = SortableArea;