记录工作中的思考🤔
背景
公司web项目有个需求,需要实现在列表内和列表之间进行元素拖放排序的功能。
项目是使用react开发的,所以起初是想找一个react拖放插件来实现,于是百度了几个拖放插件,光看了看用法就挺麻烦的(说实话,也没太仔细看)。因为需要的不只是一个拖放的展示效果,还需要收集到拖放后的新数据做逻辑,感觉用插件会复杂的多,于是,用h5的拖放api搞起来吧。
介绍一下h5 拖放api
有拖有放才叫拖放(开始我还一直错误的叫拖拽功能 拖拽api,有这个解释后,清晰明了)
【拖】方:
【属性】: draggable
设置元素为可拖放
首先,为了使元素可拖动,给元素设置draggable属性,并且把 draggable 属性值设置为 true
【事件】
- ondragstart 按下鼠标键并开始移动时触发(对象是拖拽元素)
- ondrag 在元素拖拽过程中持续触发(对象是拖拽元素)
- ondragend 元素拖拽停止时触发(对象是拖拽元素)
【放】方:
元素被拖动到有效的放置目标时,下列事件会依次发生: 【事件】
- ondragenter:当拖拽对象进入投放区时触发(对象是目标元素)
- ondragover :拖拽对象在投放区内移动时持续触发(对象是目标元素)
- ondragleave:元素离开投放区时触发(对象是目标元素)
- ondrop :拖拽对象放置在投放区时触发(对象是目标元素)
ondragover 需要注意的是:默认地,无法将数据/元素放置到其他元素中。如果需要设置允许放置,我们必须阻止对元素的默认处理方式。这要通过调用ondragover 事件的 event.preventDefault() 方法。在ondragover中一定要执行 preventDefault()否则ondrop事件不会被触发
ondrop 需要注意的是 :调用 preventDefault() 来避免浏览器对数据的默认处理(drop 事件的默认行为是以链接形式打开)
还有一个dataTransfer 对象记录数据。因为react 有state或者是作为数据管理mobx或redux,就直接没用到。可以自行百度
拖拽过程
拖动元素的事件
当按住鼠标拖动draggable元素的时候会按照如下顺序依次触发
ondragstart -> ondrag -> ondragend
投放区事件
当将draggable元素元素拖动到容器中将会按照如下顺序依次触发
ondragenter -> ondragover -> drop
实现
之前在查资料过程中遇到了一遍文章,然后接下来的实现样式和部分思路是来源于原链接
只有拖放没有排序
效果:
/**
* 主js
主要思路就是:
1、使用【拖】方方法 ondragstart ondragend 记录拖拽元素的信息
2、使用【接收方】方法 ondragenter ondragleave 记录接收区域的信息
3、ondrop 放置的时候,开始处理对应区域的数组内容
*/
import React, { Component } from 'react';
import TaskCol from './TaskCol.js';//接收区域
import TaskItem from './TaskItem.js';//接收区域的每一个可拖拽元素
const STATUS_TODO = 'STATUS_TODO';
const STATUS_DOING = 'STATUS_DOING';
const STATUS_DONE = 'STATUS_DONE';
//按照这样一个数据结构
const TASK_DATA = [{
id: STATUS_TODO,
title: '待处理',
data: [{
id: 1,
status: STATUS_TODO,
content: '写一个react拖拽实现记录',
},
{
id: 2,
status: STATUS_TODO,
content: '搞懂redux中间件源码',
}, {
id: 3,
status: STATUS_TODO,
content: '学习react-router',
}, {
id: 4,
status: STATUS_TODO,
content: '学习css',
}, {
id: 5,
status: STATUS_TODO,
content: '学习算法',
},
{
id: 6,
status: STATUS_TODO,
content: '前端学习之路真的是无止境',
},
{
id: 7,
status: STATUS_TODO,
content: '有工作 也有生活才够好',
},
{
id: 8,
status: STATUS_TODO,
content: '有生活又有乐趣才完美',
},
{
id: 9,
status: STATUS_TODO,
content: '加油吧 你可以的',
},
],
},
{
id: STATUS_DOING,
title: '进行中',
data: [],
},
{
id: STATUS_DONE,
title: '已完成',
data: [],
},
];
const defaultFrom = { // 记录拖方信息
itemIndex: null, // 拖拽元素在当前所在区域列的位置
colId: null, // 拖拽元素所在的区域列的id(唯一标识)
item: null, // 拖拽元素的信息
};
const defaultTo = { // 记录接收方信息
colId: null, // 接收区域列的id(唯一标识)
};
export default class Drag extends Component {
constructor(props) {
super(props);
this.state = {
tasks: TASK_DATA,
from: defaultFrom,
to: defaultTo,
};
}
handleDragStart = (itemIndex, colId, item) => {
this.setState({
from: {
itemIndex,
colId,
item,
},
});
}
handleDragEnd = () => {
this.setState({
from: {},
});
}
/**
* 接收方
*/
handleDragEnter = (colId) => {
this.setState({
to: {
colId,
},
});
}
handleDragLeave = () => {
this.setState({
to: defaultTo,
});
}
handleDrop = () => {
const tasks = this.state.tasks;
const { from, to } = this.state;
const { itemIndex, item } = from;
if (from.colId === to.colId) { // 如果在元素所在的区域拖放,不做处理
return;
}
tasks.forEach((taskItem) => { // 把原列表拖拽的元素删除,push到新列表里
if (taskItem.id === from.colId) {
taskItem.data.splice(itemIndex, 1);
} else if (taskItem.id === to.colId) {
taskItem.data.push(item);
}
});
this.setState({
tasks,
from: defaultFrom,
to: defaultTo,
});
}
render() {
const { tasks, from, to } = this.state;
return (<div>
<div style={{ display: 'flex', height: 400, width: 700 }}>
{
tasks.map(item => < TaskCol
id={item.id}
title={item.title}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDrop={this.handleDrop}
active={from.itemIndex && from.colId !== to.colId && item.id === to.colId}//用来控制样式的
>
{
item.data.map((i, index) =>
<TaskItem
index={index}
data={i}
colId={item.id} // 所在区域的id
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
active={from.itemIndex === index}//用来控制样式的
/>)
} </TaskCol>)
} </div>
</div>);
}
}
/**
接收区域的js文件
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './index.css';
export default class TaskCol extends Component {
onDragEnter=() => {
// 记录进入哪一个任务列区域了
const { id, onDragEnter } = this.props;
onDragEnter(id);
}
onDragOver=(e) => {
// 默认地,无法将数据/元素放置到其他元素中。如果需要设置允许放置,我们必须阻止对元素的默认处理方式。这要通过调用ondragover 事件的 event.preventDefault()
// 如果不阻止dragover的默认行为,drop就不会触发
e.preventDefault();
}
render() {
const { title, children, active } = this.props;
return (<div className={styles.colWrapper}>
<div className={styles.colHeader}>{title}</div>
<div className={`${styles.listWrapper} ${active && styles.activeListWrapper}`}
onDragEnter={this.onDragEnter}
onDragOver={this.onDragOver}
onDrop={this.props.onDrop}
>
{children}
</div>
</div>);
}
}
TaskCol.propTypes = {
title: PropTypes.string,
id: PropTypes.string,
onDrop: PropTypes.func,
onDragEnter: PropTypes.func,
active: PropTypes.bool,
children: PropTypes.node,
};
TaskCol.defaultProps = {
onDragLeave: () => {},
onDragEnter: () => {},
};
/**
可拖放的元素
*/
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './index.css';
export default class TaskItem extends Component {
onDragStart=() => {
// 我们能知道拖拽的是哪个元素
const { onDragStart, index, colId, data } = this.props;
onDragStart(index, colId, data);
}
render() {
const { data, active } = this.props;
return (<div
onDragStart={this.onDragStart}
onDragEnd={this.props.onDragEnd}
draggable //可拖拽的属性
className={`${styles.item} ${active && styles.activeItem}`}
>
{data.content}
</div>);
}
}
TaskItem.propTypes = {
data: PropTypes.object,
index: PropTypes.number,
colId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), // 所在区域的标识
onDragEnd: PropTypes.func,
onDragStart: PropTypes.func,
active: PropTypes.bool,
};
<!--样式 使用的是css modules-->
.colWrapper{
border: 1px solid #d2d2d2;
flex-grow: 1;
width: 180px;
height: 100%;
margin: 0 2px;
background: #eee;
flex-grow: 1;
display: flex;
flex-direction: column;
margin-right: 5px
}
.colHeader{
height: 40px;
line-height: 40px;
background: #1DA921;
color: #fff;
text-align: center;
}
.listWrapper{
overflow: auto;
height: 100%;
}
.activeListWrapper{
background: #00ad23;
opacity: 0.1;
}
.item{
border: 1px solid #1da921;
width: 180px;
border-radius: 5px;
box-shadow: 0 0 5px 0 #b3b3b3;
margin: 5px auto;
background: #fff;
height: 40px;
line-height: 40px;
padding: 0 10px;
width: 200px;
cursor: move
}
.activeItem {
border-style: dashed;
}
加上排序
首先要说的是 我这个排序 是建立在每个item高度一致的基础上。如果高度不一,现在我还不知道该怎么实现。可排序的主要思路在于,在拖动过程中,记录鼠标移到了接收区的第几个元素位置上((scrollTop+offsetY)/单个元素高度),然后原区域列表删掉元素,新区域增加元素
最新代码
import React, { Component } from 'react';
import TaskCol from './TaskCol.js';
import TaskItem from './TaskItem.js';
const STATUS_TODO = 'STATUS_TODO';
const STATUS_DOING = 'STATUS_DOING';
const STATUS_DONE = 'STATUS_DONE';
const TASK_DATA = [{
id: STATUS_TODO,
title: '待处理',
data: [{
id: 1,
status: STATUS_TODO,
content: '写一个react拖拽实现记录',
},
{
id: 2,
status: STATUS_TODO,
content: '搞懂redux中间件源码',
}, {
id: 3,
status: STATUS_TODO,
content: '学习react-router',
}, {
id: 4,
status: STATUS_TODO,
content: '学习css',
}, {
id: 5,
status: STATUS_TODO,
content: '学习算法',
},
{
id: 6,
status: STATUS_TODO,
content: '前端学习之路真的是无止境',
},
{
id: 7,
status: STATUS_TODO,
content: '有工作 也有生活才够好',
},
{
id: 8,
status: STATUS_TODO,
content: '有生活又有乐趣才完美',
},
{
id: 9,
status: STATUS_TODO,
content: '加油吧 你可以的',
},
],
},
{
id: STATUS_DOING,
title: '进行中',
data: [],
},
{
id: STATUS_DONE,
title: '已完成',
data: [],
},
];
const placeholder = <div style={{ height: 40, border: '1px dashed pink' }} />;
const defaultFrom = { // 拖方信息
itemIndex: null, // 拖拽元素在当前所在区域列的位置
colId: null, // 拖拽元素所在的区域列的id(唯一标识)
item: null, // 拖拽元素全部的信息
};
const defaultTo = { // 接收方信息
colId: null, // 接收区域列的id(唯一标识)
toIndex: null, // 接收区域接收的元素位置 (排序新增)
};
export default class AdvancedDrag extends Component {
constructor(props) {
super(props);
this.state = {
tasks: TASK_DATA,
from: defaultFrom,
to: defaultTo,
};
}
handleDragStart = (itemIndex, colId, item) => {
this.setState({
from: {
itemIndex,
colId,
item,
},
});
}
handleDragEnd = () => {
this.setState({
from: {},
});
}
/**
* 接收方
*/
handleDragEnter = (colId) => {
this.setState({
to: {
...this.state.to,
colId,
},
});
}
handleDragLeave = () => {
this.setState({
to: defaultTo,
});
}
getContainer=(ele) => {
// 准确的找到接收方
while (!ele.dataset.id) {
ele = ele.parentNode;
}
return ele;
}
handleDragOver=(e) => {(排序新增)
// (拿到当前区域的scrollTop+ 鼠标所在元素距离区域顶部的offsetTop)/元素高度 = 鼠标在第几个元素上
// 直接e.offsetTop e.target结果是null。正确用法是从e.nativeEvent里取值
const srcollDiv = this.getContainer(e.nativeEvent.target);
const scrollTop = srcollDiv.scrollTop;
// 实际上这个layerY属性的值才是距离顶部的距离。注意的是!!item元素不要加postion或者overflow属性,会影响到这layer值的准确性。虽然我也不知道为什么
const offsetY = e.nativeEvent.layerY;
const index = Math.floor((scrollTop + offsetY) / 40);// 40是item的高度
if (index !== this.state.to.toIndex) {
this.setState({ to: { ...this.state.to, toIndex: index } });
}
}
handleDrop = () => {
const tasks = this.state.tasks;
const { from, to } = this.state;
const { itemIndex, item } = from;
tasks.forEach((taskItem) => {
if (taskItem.id === from.colId && taskItem.id === to.colId) {
taskItem.data.splice(itemIndex, 1);
taskItem.data.splice(to.toIndex, 0, item);
} else if (taskItem.id === from.colId) {
taskItem.data.splice(itemIndex, 1);
} else if (taskItem.id === to.colId) {
taskItem.data.splice(to.toIndex, 0, item);// 这加一
}
});
this.setState({
tasks,
from: defaultFrom,
to: defaultTo,
});
}
render() {
const { tasks, from, to } = this.state;
return (<div >
<div> 有拖放 有排序 </div>
<div style={{ display: 'flex', height: 400, width: 700 }}>
{
tasks.map(item => < TaskCol
id={item.id}
title={item.title}
onDragEnter={this.handleDragEnter}
onDragOver={this.handleDragOver}
onDragLeave={this.handleDragLeave}
onDrop={this.handleDrop}(排序新增)
active={from.itemIndex && from.colId !== to.colId && item.id === to.colId}
>
//(排序新增)
{
item.data.map((i, index) => {
const div = (<TaskItem
index={index}
data={i}
colId={item.id} // 所在区域的id
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
active={from.itemIndex === index && to.colId === item.id}
/>);
// 这个占位样式加的 其实没有那么的准确。并不完全是 占位在哪,元素就会移到哪里。最准确的是元素会放在新元素上就会占据新元素的位置 越看也没看明白
return to.colId === item.id && to.toIndex === index ? [div, placeholder] : div;
})
} </TaskCol>)
} </div>
</div>);
}
}
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import styles from './index.css';
export default class TaskCol extends Component {
onDragEnter=() => {
// 记录进入哪一个任务列区域了
const { id, onDragEnter } = this.props;
onDragEnter(id);
}
onDragOver=(e) => {
// 默认地,无法将数据/元素放置到其他元素中。如果需要设置允许放置,我们必须阻止对元素的默认处理方式。这要通过调用ondragover 事件的 event.preventDefault()
// 如果不阻止dragover的默认行为,drop就不会触发
e.preventDefault();
this.props.onDragOver(e);(排序新增)
}
render() {
const { title, children, active, id } = this.props;
return (<div className={styles.colWrapper}>
<div className={styles.colHeader}>{title}</div>
<div
className={`${styles.listWrapper} ${active && styles.activeListWrapper}`}
data-id={id}//(排序新增 用来精准找到 滚动元素)
onDragEnter={this.onDragEnter}
onDragOver={this.onDragOver}
onDrop={this.props.onDrop}
>
{children}
</div>
</div>);
}
}
TaskCol.propTypes = {
title: PropTypes.string,
id: PropTypes.string,
onDrop: PropTypes.func,
onDragEnter: PropTypes.func,
active: PropTypes.bool,
children: PropTypes.node,
onDragOver: PropTypes.func,
};
TaskCol.defaultProps = {
onDragLeave: () => {},
onDragEnter: () => {},
onDragOver: () => {},(排序新增)
};
其他文件没变化
整体上可以实现拖放排序效果了,确实还有些许瑕疵。先记录一番吧。