Skip to content

Commit 40ddb31

Browse files
authored
Merge pull request #70 from fritz-c/can-drop-api
Add `canDrop` API
2 parents 8023f13 + a52cf40 commit 40ddb31

10 files changed

+220
-62
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ generateNodeProps | func | | | Ge
5858
getNodeKey | func | defaultGetNodeKey | | Determine the unique key used to identify each node and generate the `path` array passed in callbacks. By default, returns the index in the tree (omitting hidden nodes).<div>`({ node: object, treeIndex: number }): string or number`</div>
5959
onMoveNode | func | | | Called after node move operation. <div>`({ treeData: object[], node: object, treeIndex: number, path: number[] or string[] }): void`</div>
6060
onVisibilityToggle | func | | | Called after children nodes collapsed or expanded. <div>`({ treeData: object[], node: object, expanded: bool }): void`</div>
61+
canDrop | func | | | Return false to prevent node from dropping in the given location. <div>`({ node: object, prevPath: number[] or string[], prevParent: object, nextPath: number[] or string[], nextParent: object}): bool`</div>
6162
reactVirtualizedListProps | object | | | Custom properties to hand to the [react-virtualized list](https://github.com/bvaughn/react-virtualized/blob/master/docs/List.md#prop-types)
6263
rowHeight | number or func | `62` | | Used by react-virtualized. Either a fixed row height (number) or a function that returns the height of a row given its index: `({ index: number }): number`
6364
slideRegionSize | number | `100` | | Size in px of the region near the edges that initiates scrolling on dragover.

src/examples/basicExample/app.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,11 @@ class App extends Component {
124124
},
125125
],
126126
},
127+
{
128+
title: 'You cannot give this children',
129+
subtitle: 'Dropping is prevented via the `canDrop` API using `nextParent`',
130+
noChildren: true,
131+
},
127132
{
128133
title: 'When node contents are really long, it will cause a horizontal scrollbar' +
129134
' to appear. Deeply nested elements will also trigger the scrollbar.',
@@ -282,6 +287,7 @@ class App extends Component {
282287
maxDepth={maxDepth}
283288
searchQuery={searchString}
284289
searchFocusOffset={searchFocusIndex}
290+
canDrop={({ nextParent }) => !nextParent || !nextParent.noChildren}
285291
searchFinishCallback={matches =>
286292
this.setState({
287293
searchFoundCount: matches.length,

src/node-renderer-default.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ const NodeRendererDefault = ({
2121
connectDragPreview,
2222
connectDragSource,
2323
isDragging,
24-
isOver,
2524
canDrop,
2625
node,
2726
draggedNode,
@@ -32,8 +31,11 @@ const NodeRendererDefault = ({
3231
buttons,
3332
className,
3433
style = {},
35-
startDrag: _startDrag,
36-
endDrag: _endDrag,
34+
didDrop,
35+
isOver: _isOver, // Not needed, but preserved for other renderers
36+
parentNode: _parentNode, // Needed for drag-and-drop utils
37+
endDrag: _endDrag, // Needed for drag-and-drop utils
38+
startDrag: _startDrag, // Needed for drag-and-drop utils
3739
...otherProps,
3840
}) => {
3941
let handle;
@@ -66,6 +68,7 @@ const NodeRendererDefault = ({
6668
}
6769

6870
const isDraggedDescendant = draggedNode && isDescendant(draggedNode, node);
71+
const isLandingPadActive = !didDrop && isDragging;
6972

7073
return (
7174
<div
@@ -95,8 +98,8 @@ const NodeRendererDefault = ({
9598
{connectDragPreview(
9699
<div
97100
className={styles.row +
98-
(isDragging && isOver ? ` ${styles.rowLandingPad}` : '') +
99-
(isDragging && !isOver && canDrop ? ` ${styles.rowCancelPad}` : '') +
101+
(isLandingPadActive ? ` ${styles.rowLandingPad}` : '') +
102+
(isLandingPadActive && !canDrop ? ` ${styles.rowCancelPad}` : '') +
100103
(isSearchMatch ? ` ${styles.rowSearchMatch}` : '') +
101104
(isSearchFocus ? ` ${styles.rowSearchFocus}` : '') +
102105
(className ? ` ${className}` : '')
@@ -163,13 +166,15 @@ NodeRendererDefault.propTypes = {
163166
// Drag source
164167
connectDragPreview: PropTypes.func.isRequired,
165168
connectDragSource: PropTypes.func.isRequired,
169+
parentNode: PropTypes.object, // Needed for drag-and-drop utils
166170
startDrag: PropTypes.func.isRequired, // Needed for drag-and-drop utils
167171
endDrag: PropTypes.func.isRequired, // Needed for drag-and-drop utils
168172
isDragging: PropTypes.bool.isRequired,
173+
didDrop: PropTypes.bool.isRequired,
169174
draggedNode: PropTypes.object,
170175
// Drop target
171176
isOver: PropTypes.bool.isRequired,
172-
canDrop: PropTypes.bool.isRequired,
177+
canDrop: PropTypes.bool,
173178
};
174179

175180
export default NodeRendererDefault;

src/react-sortable-tree.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ import {
2020
getDescendantCount,
2121
find,
2222
} from './utils/tree-data-utils';
23+
import {
24+
memoizedInsertNode,
25+
} from './utils/memoized-tree-data-utils';
2326
import {
2427
swapRows,
2528
} from './utils/generic-utils';
@@ -233,12 +236,13 @@ class ReactSortableTree extends Component {
233236
}
234237

235238
dragHover({ node: draggedNode, depth, minimumTreeIndex }) {
236-
const addedResult = insertNode({
239+
const addedResult = memoizedInsertNode({
237240
treeData: this.state.draggingTreeData,
238241
newNode: draggedNode,
239242
depth,
240243
minimumTreeIndex,
241244
expandParent: true,
245+
getNodeKey: this.props.getNodeKey,
242246
});
243247

244248
const rows = this.getRows(addedResult.treeData);
@@ -397,7 +401,14 @@ class ReactSortableTree extends Component {
397401
);
398402
}
399403

400-
renderRow({ node, path, lowerSiblingCounts, treeIndex }, listIndex, key, style, getPrevRow, matchKeys) {
404+
renderRow(
405+
{ node, parentNode, path, lowerSiblingCounts, treeIndex },
406+
listIndex,
407+
key,
408+
style,
409+
getPrevRow,
410+
matchKeys
411+
) {
401412
const TreeNodeRenderer = this.treeNodeRenderer;
402413
const NodeContentRenderer = this.nodeContentRenderer;
403414
const nodeKey = path[path.length - 1];
@@ -407,6 +418,7 @@ class ReactSortableTree extends Component {
407418

408419
const nodeProps = !this.props.generateNodeProps ? {} : this.props.generateNodeProps({
409420
node,
421+
parentNode,
410422
path,
411423
lowerSiblingCounts,
412424
treeIndex,
@@ -421,6 +433,9 @@ class ReactSortableTree extends Component {
421433
treeIndex={treeIndex}
422434
listIndex={listIndex}
423435
getPrevRow={getPrevRow}
436+
treeData={this.state.draggingTreeData || this.state.treeData}
437+
getNodeKey={this.props.getNodeKey}
438+
customCanDrop={this.props.canDrop}
424439
node={node}
425440
path={path}
426441
lowerSiblingCounts={lowerSiblingCounts}
@@ -433,6 +448,7 @@ class ReactSortableTree extends Component {
433448
>
434449
<NodeContentRenderer
435450
node={node}
451+
parentNode={parentNode}
436452
path={path}
437453
isSearchMatch={isSearchMatch}
438454
isSearchFocus={isSearchFocus}
@@ -527,6 +543,9 @@ ReactSortableTree.propTypes = {
527543
// Called after node move operation.
528544
onMoveNode: PropTypes.func,
529545

546+
// Determine whether a node can be dropped based on its path and parents'.
547+
canDrop: PropTypes.func,
548+
530549
// Called after children nodes collapsed or expanded.
531550
onVisibilityToggle: PropTypes.func,
532551

src/tree-node.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,14 @@ const TreeNode = ({
1414
draggedNode,
1515
canDrop,
1616
treeIndex,
17-
getPrevRow: _getPrevRow, // Delete from otherProps
18-
node: _node, // Delete from otherProps
19-
path: _path, // Delete from otherProps
20-
maxDepth: _maxDepth, // Delete from otherProps
21-
dragHover: _dragHover, // Delete from otherProps
17+
customCanDrop: _customCanDrop, // Delete from otherProps
18+
dragHover: _dragHover, // Delete from otherProps
19+
getNodeKey: _getNodeKey, // Delete from otherProps
20+
getPrevRow: _getPrevRow, // Delete from otherProps
21+
maxDepth: _maxDepth, // Delete from otherProps
22+
node: _node, // Delete from otherProps
23+
path: _path, // Delete from otherProps
24+
treeData: _treeData, // Delete from otherProps
2225
...otherProps,
2326
}) => {
2427
// Construct the scaffold representing the structure of the tree
@@ -147,7 +150,7 @@ TreeNode.propTypes = {
147150
// Drop target
148151
connectDropTarget: PropTypes.func.isRequired,
149152
isOver: PropTypes.bool.isRequired,
150-
canDrop: PropTypes.bool.isRequired,
153+
canDrop: PropTypes.bool,
151154
draggedNode: PropTypes.object,
152155
};
153156

src/utils/drag-and-drop-utils.js

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@ import HTML5Backend from 'react-dnd-html5-backend';
77
import {
88
getDepth,
99
} from './tree-data-utils';
10+
import {
11+
memoizedInsertNode,
12+
} from './memoized-tree-data-utils';
1013

1114
const nodeDragSource = {
1215
beginDrag(props) {
1316
props.startDrag(props);
1417

1518
return {
16-
node: props.node,
17-
path: props.path,
19+
node: props.node,
20+
parentNode: props.parentNode,
21+
path: props.path,
1822
};
1923
},
2024

@@ -57,28 +61,43 @@ function getTargetDepth(dropTargetProps, monitor) {
5761
return targetDepth;
5862
}
5963

60-
function canDrop(dropTargetProps, monitor, isHover = false) {
61-
let abovePath = [];
62-
let aboveNode = {};
63-
const rowAbove = dropTargetProps.getPrevRow();
64-
if (rowAbove) {
65-
abovePath = rowAbove.path;
66-
aboveNode = rowAbove.node;
64+
function canDrop(dropTargetProps, monitor) {
65+
if (!monitor.isOver()) {
66+
return false;
6767
}
6868

69+
const rowAbove = dropTargetProps.getPrevRow();
70+
const abovePath = rowAbove ? rowAbove.path : [];
71+
const aboveNode = rowAbove ? rowAbove.node : {};
6972
const targetDepth = getTargetDepth(dropTargetProps, monitor);
70-
const draggedNode = monitor.getItem().node;
71-
return (
72-
// Either we're not adding to the children of the row above...
73-
targetDepth < abovePath.length ||
74-
// ...or we guarantee it's not a function we're trying to add to
75-
typeof aboveNode.children !== 'function'
76-
) && (
77-
// Ignore when hovered above the identical node...
78-
!(dropTargetProps.node === draggedNode && isHover === true) ||
79-
// ...unless it's at a different level than the current one
80-
targetDepth !== (dropTargetProps.path.length - 1)
81-
);
73+
74+
// Cannot drop if we're adding to the children of the row above and
75+
// the row above is a function
76+
if (targetDepth >= abovePath.length && typeof aboveNode.children === 'function') {
77+
return false;
78+
}
79+
80+
if (typeof dropTargetProps.customCanDrop === 'function') {
81+
const node = monitor.getItem().node;
82+
const addedResult = memoizedInsertNode({
83+
treeData: dropTargetProps.treeData,
84+
newNode: node,
85+
depth: targetDepth,
86+
getNodeKey: dropTargetProps.getNodeKey,
87+
minimumTreeIndex: dropTargetProps.listIndex,
88+
expandParent: true,
89+
});
90+
91+
return dropTargetProps.customCanDrop({
92+
node,
93+
prevPath: monitor.getItem().path,
94+
prevParent: monitor.getItem().parentNode,
95+
nextPath: addedResult.path,
96+
nextParent: addedResult.parentNode,
97+
});
98+
}
99+
100+
return true;
82101
}
83102

84103
const nodeDropTarget = {
@@ -92,15 +111,24 @@ const nodeDropTarget = {
92111
},
93112

94113
hover(dropTargetProps, monitor) {
95-
if (!canDrop(dropTargetProps, monitor, true)) {
114+
const targetDepth = getTargetDepth(dropTargetProps, monitor);
115+
const draggedNode = monitor.getItem().node;
116+
const needsRedraw = (
117+
// Redraw if hovered above different nodes
118+
dropTargetProps.node !== draggedNode ||
119+
// Or hovered above the same node but at a different depth
120+
targetDepth !== (dropTargetProps.path.length - 1)
121+
);
122+
123+
if (!needsRedraw) {
96124
return;
97125
}
98126

99127
dropTargetProps.dragHover({
100-
node: monitor.getItem().node,
128+
node: draggedNode,
101129
path: monitor.getItem().path,
102130
minimumTreeIndex: dropTargetProps.listIndex,
103-
depth: getTargetDepth(dropTargetProps, monitor),
131+
depth: targetDepth,
104132
});
105133
},
106134

@@ -112,6 +140,7 @@ function nodeDragSourcePropInjection(connect, monitor) {
112140
connectDragSource: connect.dragSource(),
113141
connectDragPreview: connect.dragPreview(),
114142
isDragging: monitor.isDragging(),
143+
didDrop: monitor.didDrop(),
115144
};
116145
}
117146

src/utils/memoized-tree-data-utils.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { insertNode } from './tree-data-utils';
2+
3+
let memoizedInsertArgsArray = [];
4+
let memoizedInsertKeysArray = [];
5+
let memoizedInsertResult = null;
6+
7+
/**
8+
* Insert a node into the tree at the given depth, after the minimum index
9+
*
10+
* @param {!Object[]} treeData - Tree data
11+
* @param {!number} depth - The depth to insert the node at (the first level of the array being depth 0)
12+
* @param {!number} minimumTreeIndex - The lowest possible treeIndex to insert the node at
13+
* @param {!Object} newNode - The node to insert into the tree
14+
* @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
15+
* @param {boolean=} expandParent - If true, expands the parent of the inserted node
16+
* @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
17+
*
18+
* @return {Object} result
19+
* @return {Object[]} result.treeData - The tree data with the node added
20+
* @return {number} result.treeIndex - The tree index at which the node was inserted
21+
* @return {number[]|string[]} result.path - Array of keys leading to the node location after insertion
22+
*/
23+
export function memoizedInsertNode(args) {
24+
const keysArray = Object.keys(args).sort();
25+
const argsArray = keysArray.map(key => args[key]);
26+
27+
// If the arguments for the last insert operation are different than this time,
28+
// recalculate the result
29+
if (argsArray.length !== memoizedInsertArgsArray.length ||
30+
argsArray.some((arg, index) => arg !== memoizedInsertArgsArray[index]) ||
31+
keysArray.some((key, index) => key !== memoizedInsertKeysArray[index])
32+
) {
33+
memoizedInsertArgsArray = argsArray;
34+
memoizedInsertKeysArray = keysArray;
35+
memoizedInsertResult = insertNode(args);
36+
}
37+
38+
return memoizedInsertResult;
39+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
insertNode,
3+
} from './tree-data-utils';
4+
5+
import {
6+
memoizedInsertNode,
7+
} from './memoized-tree-data-utils';
8+
9+
describe('insertNode', () => {
10+
it('should handle empty data', () => {
11+
const params = {
12+
treeData: [],
13+
depth: 0,
14+
minimumTreeIndex: 0,
15+
newNode: {},
16+
getNodeKey: ({ treeIndex }) => treeIndex,
17+
};
18+
19+
expect(insertNode(params) === insertNode(params)).toEqual(false);
20+
expect(memoizedInsertNode(params) === memoizedInsertNode(params)).toEqual(true);
21+
expect(memoizedInsertNode(params) === memoizedInsertNode({...params, treeData: [{}]})).toEqual(false);
22+
});
23+
});

0 commit comments

Comments
 (0)