拖拽排序是一个业务开发过程中很常见的需求,而仅仅为了实现 pc 上纵向列表拖拽排序,引入一个庞大的拖拽组件库,了解一堆看起来就很复杂的 api ,不禁让人生出一种杀鸡焉用牛刀的怀疑…
简单点的方法有没有?有~
组件接口设计
Sortable 将是一个受控的排序组件,调用方提供 data、handleChange 和单项 render 方法:
const initialData = Array.from({ length: 20 }).map((_, index) => ({
id: index,
text: `item ${index}`
}));
const [data, setData] = useState(initialData);
const handleChange = value => {
setData(value);
};
<Sortable
data={data}
handleChange={handleChange}
render={item => (
<div>
<a href="" style={{ color: "#333", textDecoration: "none" }}>
{item.text}
</a>
</div>
)}
/>
组件实现
初始状态设计: dragging 表示拖动状态,draggingIndex 表示当前拖动的 item 在列表中的 index ,startPageY 是开始拖动时的位置,
state = { dragging: false, draggingIndex: -1, startPageY: 0 };
Map data,mouseDown 绑定在每个 item 上:
<div className=“sortable-list”>
{this.props.data.map((item, i) => (
<div
className="sortable-item"
key={i}
onMouseDown={event => this.handleMounseDown(event, i)}
>
{this.props.render(item)}
</div>
))}
</div>
onMouseDown 时,记录初始状态:
handleMounseDown = (event, index) => {
this.setState({
dragging: true,
startPageY: event.pageY,
draggingIndex: index
});
};
很明显,接下来我需要监听 onMouseMove 和 mouseUp 事件,判断我拖动的条目拖动的方向,以及停下的位置。一个重要的问题来了,onMouseMove 和 mouseUp 应该绑定在什么地方?document 吗? 整个 List 吗?
我在 list 上方加了一个透明的 mask ,当有拖拽行为的时候把 mask 渲染出来,这样做的好处是,拖动时鼠标不会响应其它区域的元素 :
<div className=“sortable-list”>….</div>
{this.state.dragging && (
<div
className="sortable-mask"
onMouseMove={this.handleMouseMove}
onMouseUp={this.handleMouseUp}
/>
)}
onMouseMove 需要判断当前拖动的方向,及在这个方向上的拖动位移。当拖动「一定距离」时,交换相邻的两个 item ,为了让拖动体验更流畅,我把「一定距离」规定为 item 行高的一半。为了计算行高,我给 item 加了个 ref:
draggingItemRef = React.createRef();
…
{this.props.data.map((item, i) => (
<div
ref={i === this.state.draggingIndex ? this.draggingItemRef : null}
className="sortable-item"
key={i}
onMouseDown={evt => this.handleMounseDown(evt, i)}
>
{this.props.render(item)}
</div>
))}
handleMouseMove = evt => {
const lineHeight = this.draggingItemRef.current.getBoundingClientRect()
.height;
let offset = evt.pageY - this.state.startPageY;
const draggingIndex = this.state.draggingIndex;
if (offset > lineHeight / 2 && draggingIndex < this.props.data.length - 1) {
// move down
this.setState({
draggingIndex: draggingIndex + 1,
startPageY: this.state.startPageY + lineHeight
});
this.props.handleChange(
move(this.props.data, draggingIndex, draggingIndex + 1)
);
} else if (offset < -lineHeight / 2 && draggingIndex > 0) {
// move up
this.setState({
draggingIndex: draggingIndex - 1,
startPageY: this.state.startPageY - lineHeight
});
this.props.handleChange(
move(this.props.data, draggingIndex, draggingIndex - 1)
);
}
};
mouseUp 就简单了,还原 state 到拖拽前就好了:
handleMouseUp = () => {
this.setState({ dragging: false, startPageY: 0, draggingIndex: -1 });
};
看看效果:
被拖拽的 item 应该有个位移,或者透明等和其它 item 不同的样式,我用 transform 来实现,那么需要新增一个 state —— offsetPageY, mouseMove 时也应该及时更新 offsetPageY:
state = {
dragging: false,
draggingIndex: -1,
startPageY: 0,
offsetPageY: 0
};
handleMouseMove = evt => {
const lineHeight = this.draggingItemRef.current.getBoundingClientRect()
.height;
let offset = evt.pageY - this.state.startPageY;
const draggingIndex = this.state.draggingIndex;
if (offset > lineHeight / 2 && draggingIndex < this.props.data.length - 1) {
// move down
offset -= lineHeight;
this.setState({
draggingIndex: draggingIndex + 1,
startPageY: this.state.startPageY + lineHeight
});
this.props.handleChange(
move(this.props.data, draggingIndex, draggingIndex + 1)
);
} else if (offset < -lineHeight / 2 && draggingIndex > 0) {
// move up
offset += lineHeight;
this.setState({
draggingIndex: draggingIndex - 1,
startPageY: this.state.startPageY - lineHeight
});
this.props.handleChange(
move(this.props.data, draggingIndex, draggingIndex - 1)
);
}
this.setState({ offsetPageY: offset });
};
渲染时获取样式:
getDraggingStyle(index) {
if (index !== this.state.draggingIndex) return {};
return {
backgroundColor: "#eee",
transform: `translate(20px, ${this.state.offsetPageY}px)`,
opacity: 0.5
};
}
{this.props.data.map((item, i) => (
<div
ref={i === this.state.draggingIndex ? this.draggingItemRef : null}
className="sortable-item"
key={i}
onMouseDown={evt => this.handleMounseDown(evt, i)}
style={this.getDraggingStyle(i)}
>
{this.props.render(item)}
</div>
))}
再看看效果:
移动端
mouseDown -> touchStart ; mouseMove -> touchMove; mouseUp -> touchEnd ,同时 evt 也需要改为 evt.touches[0]