树形结构的生成,可以通过递归树形数据遍历而成
节点的思路
const Node=({ children })=>{
return (
// 展开收起的三角形图标
// 节点的图标
自定义节点内容
// 子节点
{
children && (
{children}
)
}
)
}
children-container
需要加个padding-left的样式缩进,
所以写出来后用法就如下所示,可以无限嵌套,形成基本的react 的组件解构
拖拽的思路:
- 一开始打算使用
react-dnd
组件,但是发现学习成本太高,而且文档虽然有,但是感觉还是不详细,其实只需要一个简单的拖拽调位就能满足项目需求。 - 于是决定手动写拖拽,先实现一个 DragWrapper 组件,用于包裹节点(或者直接替换节点div),使节点能拖拽
- 假设过程是,拖拽的节点称为startNode, 拖拽到endNode上释放鼠标,
拖拽容器:
{children}
这个draggable的值不能是true或者false,必须得是"true"和"false";
先是onDragStart
onDragStart: 通过e.dataTransfer.setData
,传递一个id,是startNode的id,用于后面的数据操作
const handleDragStart = e => {
e.stopPropagation();
// startNode的id
e.dataTransfer.setData('guid', node.guid);
};
第二步是onDrop
,要注意这里获取到的node.id是endNode的id,同一个组件函数里面,通过props获取的node.guid 是不一样的,因为渲染上去,这就是两个组件,onDragStart在一个节点里触发,onDrop是在另一个节点里触发的,这两个节点都是用同一个函数组件生成的,两个节点之间通过 e.dataTransfer.setData,把起始节点的guid传过来,在end节点里收取,
const handleDrop = e => {
e.stopPropagation();
const startGuid = e.dataTransfer.getData('guid');
const endGuid = node.guid;
if(startGuid!==endGuid){
1. 在树形数据里,把startNode找到,先用一个变量存起来,然后在树里面删除
2. 在把endNode找到,往后面插入startNode,即存起来的变量,
3. 更新树,重新存入redux中
}
}
可以理解成同一个函数生成的两个实例,一个start触发了另一个的end。有了startGuid和endGuid,就好操作了,
方便操作,可以写一个通用方法loop,
通过loop的第三个参数arr,我们找到了对应的id的节点后,这个arr就是这个节点所在的数组,往后面插入节点就是 arr.splice(i+1 , 0 , node);往前面插入节点就是arr.splice(i , 0 , node)
const loop = (data, key, callback) => {
data.forEach((item, index, arr) => {
if (item.guid === key) {
callback(item, index, arr);
return;
}
if (item.children) {
loop(item.children, key, callback);
}
});
};
有两个id,以及一棵树的数据,直接用js操作数据,对树节点进行删除增加即可,
位置判断
过程:
- 自己想没什么思路,参考了一下
rc-tree
rc-tree 目前用的是dropPosition,值分别为 -1,0,1,代表节点塞到上方,塞到下方,塞到里面,
判断函数的地址 https://github.com/react-component/tree/blob/master/src/util.tsx
可以看到这是判断位置的一个条件
e 是onDrop的参数,clientY 可以通过e获取到,clientY是释放鼠标的时候,鼠标相对浏览器左上角的纵坐标,获取这个y坐标,就可以得到鼠标距离浏览器顶部的距离,然后通过e.target.getBoundingClientRect(),可以得到endNode的宽高,以及top(节点距离浏览器最顶部的距离)
clientY < (top + height/2) 说明鼠标在节点的中间还偏上。
这里是拿节点的垂直中间位置去跟鼠标位置对比,可以根据这个,判断startNode是插入在endNode的上方还是下方
所以我们可以在onDragOver的时候,判断位置,设置样式,加一条红线,至少让用户能看到这个节点即将要插入的位置,在onDragLeave的时候删除样式
最后使用
可以用拖拽组件DragWrapper
把节点包裹
const Node=({ children })=>{
return (
// 展开收起的三角形图标
// 节点的图标
自定义节点内容
// 子节点
{
children && (
{children}
)
}
)
}
为什么不包裹整个node节点,而只包裹渲染的那部分。因为当节点展开的时候,子节点的拖拽,也会被认为是父节点的拖拽,所以只包裹渲染的部分即可
注意点:
有 canDrag这个函数可以根据业务需求判断是否允许拖拽,但是dom节点即便设置了 draggable="false" ,或者直接不设置draggable,在极限情况下,能卡出拖拽,拖拽的是文本内容
需要给这个节点加上css,不允许选中文字
-moz-user-select: -moz-none;
-khtml-user-select: none;
-webkit-user-select: none;
/*
Introduced in IE 10.
See http://ie.microsoft.com/testdrive/HTML5/msUserSelect/
*/
-ms-user-select: none;
user-select: none;