Skip to content

Commit 205f152

Browse files
authored
refactor: drag behavior & allowDrop (#355)
* refactor: optimize rerender caused by dragenter & leave * chore * chore * docs: draggable demo * chore: wip * refactor: drag style * refactor: drag, wip * chore * fix: test * chore * chore * chore * chore: clean code * refactor: drag behavior * chore: remove unused vars * refactor: extract dropIndicatorRender * refactor: dropPosition logic * chore: update test * refactor: optimize drag logic * fix: lint * refactor: drag logic * chore: use offset * perf * feat: allow-drop * fix: lint * docs: allow drop * docs: optimize perf by remove logger * chore * chore * chore * feat: dropTarget & dropContainer * chore: vertical indicator * chore * chore: snapshot update & lint fix * refactor: clean codes * chore: offset * feat: function draggable * fix: test * test: coverage * chore * fix: lint * test: cov * test: coverage * test: cov * feat: first node top half * fix: lint * test: cov * test: cov * test: cov * test: cov * test: cov * docs: allowDrop & draggable * chore * chore * test: fulfill test cases * docs: props.indent * test: cov * refactor: drop area * chore * fix: lint * fix: LGTM * fix: drag * fix: drag over indicator * fix: lint * fix: expand logic * chore: do not allow drag left on non-last child * refactor: don't allow dropPosition != 0 on node with children * feat: use css indent * refactor: move indent to state * docs: remove indent prop * fix: dropIndicatorRender type * refactor: default indicator renderer * fix: test * test: snapshot * fix: test case `without motionNodes` * fix: scrollTo test * fix: cov * test: use domSpy * refactor: extract drop indicator render * refactor: use single indent measurer node * test: snapshot * refactor: allowDrop parameter api * chore: add comment for tree state * refactor: remove nodeInstances by create props manually * chore: add comment * chore * test: update snapshot * refactor: remove abstract-drag-over-node-key * refactor: allow first child to drop on its parent * chore: trigger ci
1 parent 7ba6e42 commit 205f152

20 files changed

+1946
-201
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ see examples
6666
| defaultExpandParent | auto expand parent treeNodes when init | bool | true |
6767
| defaultSelectedKeys | default selected treeNodes | String[] | [] |
6868
| disabled | whether disabled the tree | bool | false |
69-
| draggable | whether can drag treeNode. (drag events are not supported in Internet Explorer 8 and earlier versions or Safari 5.1 and earlier versions.) | bool | false |
69+
| draggable | whether can drag treeNode. (drag events are not supported in Internet Explorer 8 and earlier versions or Safari 5.1 and earlier versions.) | bool \| ({ node }) => boolean | false |
7070
| expandedKeys | Controlled expand specific treeNodes | String[] | - |
7171
| filterTreeNode | filter some treeNodes as you need. it should return true | function(node) | - |
7272
| icon | customize icon. When you pass component, whose render will receive full TreeNode props as component props | element/Function(props) | - |
@@ -94,6 +94,7 @@ see examples
9494
| onSelect | click the treeNode to fire | function(selectedKeys, e:{selected: bool, selectedNodes, node, event, nativeEvent}) | - |
9595
| switcherIcon | specific the switcher icon. | ReactNode / (props: TreeNodeAttribute) => ReactNode | - |
9696
| virtual | Disable virtual scroll when `false` | boolean | - |
97+
| allowDrop | whether to allow drop on node | ({ dropNode, dropPosition }) => boolean | - |
9798

9899
### TreeNode props
99100

@@ -143,3 +144,4 @@ rc-tree is released under the MIT license.
143144
- [jqTree](http://mbraak.github.io/jqTree/)
144145
- [jquery.treeselect](http://travistidwell.com/jquery.treeselect.js/)
145146
- [angular Select Tree](http://a5hik.github.io/angular-multi-select-tree/)
147+

assets/index.less

Lines changed: 31 additions & 19 deletions
Large diffs are not rendered by default.

examples/draggable-allow-drop.jsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/* eslint-disable no-console, react/no-access-state-in-setstate */
2+
import React from 'react';
3+
import { gData } from './utils/dataUtil';
4+
import './draggable.less';
5+
import '../assets/index.less';
6+
import Tree from '../src';
7+
8+
function allowDrop({ dropNode, dropPosition }) {
9+
if (!dropNode.children) {
10+
if (dropPosition === 0) return false;
11+
}
12+
return true;
13+
}
14+
15+
class Demo extends React.Component {
16+
state = {
17+
gData,
18+
autoExpandParent: true,
19+
expandedKeys: ['0-0-key', '0-0-0-key', '0-0-0-0-key'],
20+
};
21+
22+
onDragStart = info => {
23+
console.log('start', info);
24+
};
25+
26+
onDragEnter = () => {
27+
console.log('enter');
28+
};
29+
30+
onDrop = info => {
31+
console.log('drop', info);
32+
const dropKey = info.node.key;
33+
const dragKey = info.dragNode.key;
34+
const dropPos = info.node.pos.split('-');
35+
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
36+
37+
const loop = (data, key, callback) => {
38+
data.forEach((item, index, arr) => {
39+
if (item.key === key) {
40+
callback(item, index, arr);
41+
return;
42+
}
43+
if (item.children) {
44+
loop(item.children, key, callback);
45+
}
46+
});
47+
};
48+
const data = [...this.state.gData];
49+
50+
// Find dragObject
51+
let dragObj;
52+
loop(data, dragKey, (item, index, arr) => {
53+
arr.splice(index, 1);
54+
dragObj = item;
55+
});
56+
57+
if (dropPosition === 0) {
58+
// Drop on the content
59+
loop(data, dropKey, item => {
60+
// eslint-disable-next-line no-param-reassign
61+
item.children = item.children || [];
62+
// where to insert 示例添加到尾部,可以是随意位置
63+
item.children.unshift(dragObj);
64+
});
65+
} else {
66+
// Drop on the gap (insert before or insert after)
67+
let ar;
68+
let i;
69+
loop(data, dropKey, (item, index, arr) => {
70+
ar = arr;
71+
i = index;
72+
});
73+
if (dropPosition === -1) {
74+
ar.splice(i, 0, dragObj);
75+
} else {
76+
ar.splice(i + 1, 0, dragObj);
77+
}
78+
}
79+
80+
this.setState({
81+
gData: data,
82+
});
83+
};
84+
85+
onExpand = expandedKeys => {
86+
console.log('onExpand', expandedKeys);
87+
this.setState({
88+
expandedKeys,
89+
autoExpandParent: false,
90+
});
91+
};
92+
93+
render() {
94+
return (
95+
<div className="draggable-demo">
96+
<h2>draggable with allow drop</h2>
97+
<p>node can not be dropped inside a leaf node</p>
98+
<div className="draggable-container">
99+
<Tree
100+
allowDrop={allowDrop}
101+
expandedKeys={this.state.expandedKeys}
102+
onExpand={this.onExpand}
103+
autoExpandParent={this.state.autoExpandParent}
104+
draggable
105+
onDragStart={this.onDragStart}
106+
onDrop={this.onDrop}
107+
treeData={this.state.gData}
108+
/>
109+
</div>
110+
</div>
111+
);
112+
}
113+
}
114+
115+
export default Demo;

examples/draggable.jsx

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import React from 'react';
33
import { gData } from './utils/dataUtil';
44
import './draggable.less';
55
import '../assets/index.less';
6-
import Tree, { TreeNode } from '../src';
6+
import Tree from '../src';
77

88
class Demo extends React.Component {
99
state = {
@@ -16,18 +16,15 @@ class Demo extends React.Component {
1616
console.log('start', info);
1717
};
1818

19-
onDragEnter = info => {
20-
console.log('enter', info);
21-
this.setState({
22-
expandedKeys: info.expandedKeys,
23-
});
19+
onDragEnter = () => {
20+
console.log('enter');
2421
};
2522

2623
onDrop = info => {
2724
console.log('drop', info);
28-
const dropKey = info.node.props.eventKey;
29-
const dragKey = info.dragNode.props.eventKey;
30-
const dropPos = info.node.props.pos.split('-');
25+
const dropKey = info.node.key;
26+
const dragKey = info.dragNode.key;
27+
const dropPos = info.node.pos.split('-');
3128
const dropPosition = info.dropPosition - Number(dropPos[dropPos.length - 1]);
3229

3330
const loop = (data, key, callback) => {
@@ -50,27 +47,16 @@ class Demo extends React.Component {
5047
dragObj = item;
5148
});
5249

53-
if (!info.dropToGap) {
50+
if (dropPosition === 0) {
5451
// Drop on the content
55-
loop(data, dropKey, item => {
56-
// eslint-disable-next-line no-param-reassign
57-
item.children = item.children || [];
58-
// where to insert 示例添加到尾部,可以是随意位置
59-
item.children.push(dragObj);
60-
});
61-
} else if (
62-
(info.node.props.children || []).length > 0 && // Has children
63-
info.node.props.expanded && // Is expanded
64-
dropPosition === 1 // On the bottom gap
65-
) {
6652
loop(data, dropKey, item => {
6753
// eslint-disable-next-line no-param-reassign
6854
item.children = item.children || [];
6955
// where to insert 示例添加到尾部,可以是随意位置
7056
item.children.unshift(dragObj);
7157
});
7258
} else {
73-
// Drop on the gap
59+
// Drop on the gap (insert before or insert after)
7460
let ar;
7561
let i;
7662
loop(data, dropKey, (item, index, arr) => {
@@ -98,17 +84,6 @@ class Demo extends React.Component {
9884
};
9985

10086
render() {
101-
const loop = data =>
102-
data.map(item => {
103-
if (item.children && item.children.length) {
104-
return (
105-
<TreeNode key={item.key} title={item.title}>
106-
{loop(item.children)}
107-
</TreeNode>
108-
);
109-
}
110-
return <TreeNode key={item.key} title={item.title} />;
111-
});
11287
return (
11388
<div className="draggable-demo">
11489
<h2>draggable</h2>
@@ -122,9 +97,8 @@ class Demo extends React.Component {
12297
onDragStart={this.onDragStart}
12398
onDragEnter={this.onDragEnter}
12499
onDrop={this.onDrop}
125-
>
126-
{loop(this.state.gData)}
127-
</Tree>
100+
treeData={this.state.gData}
101+
/>
128102
</div>
129103
</div>
130104
);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"postpublish": "npm run gh-pages",
4141
"lint": "eslint src/ examples/ --ext .tsx,.ts,.jsx,.js",
4242
"test": "father test",
43+
"coverage": "father test --coverage",
4344
"gh-pages": "npm run build && father doc deploy",
4445
"now-build": "npm run build"
4546
},

src/DropIndicator.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import * as React from 'react'
2+
3+
export default function DropIndicator ({
4+
dropPosition,
5+
dropLevelOffset,
6+
indent,
7+
}: {
8+
dropPosition: -1 | 0 | 1,
9+
dropLevelOffset: number,
10+
indent: number,
11+
}) {
12+
const style: React.CSSProperties = {
13+
pointerEvents: 'none',
14+
position: 'absolute',
15+
right: 0,
16+
backgroundColor: 'red',
17+
height: 2,
18+
};
19+
switch (dropPosition) {
20+
case -1:
21+
style.top = 0;
22+
style.left = -dropLevelOffset * indent;
23+
break;
24+
case 1:
25+
style.bottom = 0;
26+
style.left = -dropLevelOffset * indent;
27+
break;
28+
case 0:
29+
style.bottom = 0;
30+
style.left = indent;
31+
break;
32+
}
33+
return <div style={style} />;
34+
}

src/Indent.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,7 @@ interface IndentProps {
88
isEnd: boolean[];
99
}
1010

11-
const Indent: React.FC<IndentProps> = ({ prefixCls, level, isStart, isEnd }) => {
12-
if (!level) {
13-
return null;
14-
}
15-
11+
const Indent = ({ prefixCls, level, isStart, isEnd }: IndentProps) => {
1612
const baseClassName = `${prefixCls}-indent-unit`;
1713
const list: React.ReactElement[] = [];
1814
for (let i = 0; i < level; i += 1) {

src/NodeList.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ const MotionFlattenData: FlattenNode = {
4848

4949
export interface NodeListRef {
5050
scrollTo: ScrollTo;
51+
getIndentWidth: () => number;
5152
}
5253

5354
interface NodeListProps {
@@ -167,10 +168,12 @@ const RefNodeList: React.RefForwardingComponent<NodeListRef, NodeListProps> = (p
167168

168169
// =============================== Ref ================================
169170
const listRef = React.useRef<ListRef>(null);
171+
const indentMeasurerRef = React.useRef<HTMLDivElement>(null);
170172
React.useImperativeHandle(ref, () => ({
171173
scrollTo: scroll => {
172174
listRef.current.scrollTo(scroll);
173175
},
176+
getIndentWidth: () => indentMeasurerRef.current.offsetWidth,
174177
}));
175178

176179
// ============================== Motion ==============================
@@ -278,6 +281,22 @@ const RefNodeList: React.RefForwardingComponent<NodeListRef, NodeListProps> = (p
278281
/>
279282
</div>
280283

284+
<div
285+
className={`${prefixCls}-treenode`}
286+
aria-hidden
287+
style={{
288+
position: 'absolute',
289+
pointerEvents: 'none',
290+
visibility: 'hidden',
291+
height: 0,
292+
overflow: 'hidden',
293+
}}
294+
>
295+
<div className={`${prefixCls}-indent`}>
296+
<div ref={indentMeasurerRef} className={`${prefixCls}-indent-unit`} />
297+
</div>
298+
</div>
299+
281300
<VirtualList<FlattenNode>
282301
{...domProps}
283302
data={mergedData}
@@ -305,7 +324,7 @@ const RefNodeList: React.RefForwardingComponent<NodeListRef, NodeListProps> = (p
305324
<MotionTreeNode
306325
{...restProps}
307326
{...treeNodeProps}
308-
active={activeItem && key === activeItem.data.key}
327+
active={!!activeItem && key === activeItem.data.key}
309328
pos={pos}
310329
data={treeNode.data}
311330
isStart={isStart}

0 commit comments

Comments
 (0)