Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit faadb1f

Browse files
committedFeb 2, 2023
Draft: JSK-11518: autoSnapToLevel option frontend-collective#937
1 parent 94fe359 commit faadb1f

File tree

4 files changed

+337
-19
lines changed

4 files changed

+337
-19
lines changed
 

‎src/react-sortable-tree.js

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class ReactSortableTree extends Component {
4949
this.rowHeightRerunPlanned = false;
5050

5151
const {
52+
autoSnapToLevel,
5253
dndType,
5354
nodeContentRenderer,
5455
treeNodeRenderer,
@@ -58,6 +59,7 @@ class ReactSortableTree extends Component {
5859

5960
this.dndManager = new DndManager(this);
6061

62+
this.autoSnapToLevel = autoSnapToLevel;
6163
// Wrapping classes for use with react-dnd
6264
this.treeId = `rst__${treeIdCounter}`;
6365
treeIdCounter += 1;
@@ -306,13 +308,18 @@ class ReactSortableTree extends Component {
306308
});
307309
}
308310

309-
moveNode({
310-
node,
311-
path: prevPath,
312-
treeIndex: prevTreeIndex,
313-
depth,
314-
minimumTreeIndex,
315-
}) {
311+
moveNode(args) {
312+
313+
const {
314+
node,
315+
path: prevPath,
316+
treeIndex: prevTreeIndex,
317+
depth,
318+
minimumTreeIndex,
319+
} = args;
320+
321+
console.log('react-sortable-tree: moveNode():', args);
322+
316323
const {
317324
treeData,
318325
treeIndex,
@@ -443,7 +450,16 @@ class ReactSortableTree extends Component {
443450
return;
444451
}
445452

453+
454+
console.log('react-sortable-tree: dragHover():', {draggedNode, draggedDepth, draggedMinimumTreeIndex});
455+
456+
if (draggedDepth >= 2) {
457+
console.log('react-sortable-tree: dragHover(): stop here');
458+
}
459+
446460
this.setState(({ draggingTreeData, instanceProps }) => {
461+
// console.log('react-sortable-tree: dragHover(): tree:', {draggingTreeData, treeData: instanceProps.treeData});
462+
447463
// Fall back to the tree data if something is being dragged in from
448464
// an external element
449465
const newDraggingTreeData = draggingTreeData || instanceProps.treeData;
@@ -460,16 +476,21 @@ class ReactSortableTree extends Component {
460476
const rows = this.getRows(addedResult.treeData);
461477
const expandedParentPath = rows[addedResult.treeIndex].path;
462478

479+
const changeArgs = {
480+
treeData: newDraggingTreeData,
481+
path: expandedParentPath.slice(0, -1),
482+
newNode: ({ node }) => ({ ...node, expanded: true }),
483+
getNodeKey: this.props.getNodeKey,
484+
};
485+
486+
// console.log('react-sortable-tree: dragHover(): changeArgs:', changeArgs);
487+
console.log('react-sortable-tree: dragHover(): changeArgs.path:', changeArgs.path);
488+
463489
return {
464490
draggedNode,
465491
draggedDepth,
466492
draggedMinimumTreeIndex,
467-
draggingTreeData: changeNodeAtPath({
468-
treeData: newDraggingTreeData,
469-
path: expandedParentPath.slice(0, -1),
470-
newNode: ({ node }) => ({ ...node, expanded: true }),
471-
getNodeKey: this.props.getNodeKey,
472-
}),
493+
draggingTreeData: changeNodeAtPath(changeArgs),
473494
// reset the scroll focus so it doesn't jump back
474495
// to a search result while dragging
475496
searchFocusTreeIndex: null,
@@ -479,6 +500,9 @@ class ReactSortableTree extends Component {
479500
}
480501

481502
endDrag(dropResult) {
503+
504+
console.log('react-sortable-tree: endDrag(): dropResult:', dropResult);
505+
482506
const { instanceProps } = this.state;
483507

484508
const resetTree = () =>
@@ -597,7 +621,7 @@ class ReactSortableTree extends Component {
597621

598622
renderRow(
599623
row,
600-
{ listIndex, style, getPrevRow, matchKeys, swapFrom, swapDepth, swapLength }
624+
{ listIndex, style, getPrevRow, /* getRows, */ matchKeys, swapFrom, swapDepth, swapLength }
601625
) {
602626
const { node, parentNode, path, lowerSiblingCounts, treeIndex } = row;
603627

@@ -644,6 +668,7 @@ class ReactSortableTree extends Component {
644668
key={nodeKey}
645669
listIndex={listIndex}
646670
getPrevRow={getPrevRow}
671+
// getRows={getRows}
647672
lowerSiblingCounts={lowerSiblingCounts}
648673
swapFrom={swapFrom}
649674
swapLength={swapLength}
@@ -779,6 +804,7 @@ class ReactSortableTree extends Component {
779804
listIndex: index,
780805
style: rowStyle,
781806
getPrevRow: () => rows[index - 1] || null,
807+
// getRows: () => rows,
782808
matchKeys,
783809
swapFrom,
784810
swapDepth: draggedDepth,
@@ -808,6 +834,7 @@ class ReactSortableTree extends Component {
808834
}),
809835
},
810836
getPrevRow: () => rows[index - 1] || null,
837+
// getRows: () => rows,
811838
matchKeys,
812839
swapFrom,
813840
swapDepth: draggedDepth,
@@ -828,6 +855,8 @@ class ReactSortableTree extends Component {
828855
}
829856

830857
ReactSortableTree.propTypes = {
858+
autoSnapToLevel: PropTypes.bool,
859+
831860
dragDropManager: PropTypes.shape({
832861
getMonitor: PropTypes.func,
833862
}).isRequired,
@@ -962,6 +991,7 @@ ReactSortableTree.propTypes = {
962991
};
963992

964993
ReactSortableTree.defaultProps = {
994+
autoSnapToLevel: false,
965995
canDrag: true,
966996
canDrop: null,
967997
canNodeHaveChildren: () => true,

‎src/utils/dnd-manager.js

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,79 @@ export default class DndManager {
4949
return this.treeRef.props.maxDepth;
5050
}
5151

52+
get autoSnapToLevel() {
53+
return this.treeRef.autoSnapToLevel;
54+
}
55+
5256
get rowHeightsRecomputeRequired() {
5357
return this.treeRef.rowHeightsRecomputeRequired;
5458
}
5559

60+
autoSnap({dropTargetProps, monitor, targetDepth, targetIndex}) {
61+
const { children, getPrevRow, getRows, ...restDropTargetProps } = dropTargetProps;
62+
63+
const draggedNode = monitor.getItem().node;
64+
65+
console.log('dnd-manager: autoSnap:', {...restDropTargetProps, draggedNode, targetDepth, targetIndex});
66+
67+
let currentPath = [...dropTargetProps.path];
68+
let currentIndex;
69+
let currentTargetDepth = targetDepth;
70+
71+
do {
72+
// const parentPath = [...currentPath];
73+
// const currentIndex = parentPath.pop();
74+
// const parentIndex = parentPath.slice(-1)[0];
75+
// const rows = getRows();
76+
// const parentRow = rows[parentIndex];
77+
78+
const addedResult = memoizedInsertNode({
79+
treeData: this.treeData,
80+
newNode: draggedNode,
81+
depth: currentTargetDepth,
82+
getNodeKey: this.getNodeKey,
83+
// minimumTreeIndex: parentIndex + 1,
84+
minimumTreeIndex: targetIndex,
85+
expandParent: true,
86+
});
87+
88+
if (this.customCanDrop({
89+
node: draggedNode,
90+
prevPath: monitor.getItem().path,
91+
prevParent: monitor.getItem().parentNode,
92+
prevTreeIndex: monitor.getItem().treeIndex, // Equals -1 when dragged from external tree
93+
nextPath: addedResult.path,
94+
nextParent: addedResult.parentNode,
95+
nextTreeIndex: addedResult.treeIndex,
96+
})
97+
) {
98+
// if (currentTargetDepth > 0) {
99+
// console.log('__TEST__');
100+
// }
101+
102+
// if (currentTargetDepth === 0) {
103+
// currentTargetDepth = -1;
104+
// }
105+
106+
return {
107+
// path: [4],
108+
// targetIndex: 4,
109+
// targetDepth: -1,
110+
path: addedResult.path.slice(0, -1),
111+
targetIndex: addedResult.treeIndex,
112+
targetDepth: currentTargetDepth || -1,
113+
}
114+
}
115+
116+
// currentPath = parentPath;
117+
currentPath = [...currentPath];
118+
currentIndex = currentPath.pop();
119+
currentTargetDepth -= 1;
120+
} while (typeof currentIndex !== 'undefined')
121+
122+
return false;
123+
};
124+
56125
getTargetDepth(dropTargetProps, monitor, component) {
57126
let dropTargetDepth = 0;
58127

@@ -136,6 +205,17 @@ export default class DndManager {
136205
return false;
137206
}
138207

208+
if (this.autoSnapToLevel) {
209+
// позже произойдёт проверка
210+
return this.autoSnap({
211+
dropTargetProps,
212+
monitor,
213+
targetDepth,
214+
targetIndex:
215+
dropTargetProps.listIndex
216+
});
217+
}
218+
139219
if (typeof this.customCanDrop === 'function') {
140220
const { node } = monitor.getItem();
141221
const addedResult = memoizedInsertNode({
@@ -208,7 +288,9 @@ export default class DndManager {
208288
wrapTarget(el) {
209289
const nodeDropTarget = {
210290
drop: (dropTargetProps, monitor, component) => {
211-
const result = {
291+
console.log('dnd-manager: drop(): ', dropTargetProps);
292+
293+
let result = {
212294
node: monitor.getItem().node,
213295
path: monitor.getItem().path,
214296
treeIndex: monitor.getItem().treeIndex,
@@ -217,6 +299,18 @@ export default class DndManager {
217299
depth: this.getTargetDepth(dropTargetProps, monitor, component),
218300
};
219301

302+
if (this.lastAutoSnapResult) {
303+
304+
const {path, targetIndex, targetDepth} = this.lastAutoSnapResult;
305+
306+
result = {
307+
...result,
308+
path,
309+
treeIndex: targetIndex,
310+
depth: targetDepth,
311+
}
312+
}
313+
220314
this.drop(result);
221315

222316
return result;
@@ -228,8 +322,11 @@ export default class DndManager {
228322
* from https://github.com/frontend-collective/react-sortable-tree/issues/483#issuecomment-581139473
229323
* */
230324

325+
// eslint-disable-next-line no-param-reassign,no-underscore-dangle
326+
// dropTargetProps.__test_changed = (dropTargetProps.__test_changed || 0) + 1
327+
231328
let targetIndex = 0;
232-
const targetDepth = this.getTargetDepth(
329+
let targetDepth = this.getTargetDepth(
233330
dropTargetProps,
234331
monitor,
235332
component
@@ -258,6 +355,7 @@ export default class DndManager {
258355
if (!needsRedraw) {
259356
return;
260357
}
358+
261359
// eslint-disable-next-line react/no-find-dom-node
262360
const hoverBoundingRect = findDOMNode(
263361
component
@@ -279,14 +377,34 @@ export default class DndManager {
279377
targetIndex = dropTargetProps.treeIndex + 1;
280378
}
281379

282-
// console.log('dnd-manager: hover:', {...dropTargetProps, draggedNode, targetDepth, targetIndex});
380+
let { path } = monitor.getItem();
381+
382+
if (this.autoSnapToLevel && typeof this.customCanDrop === 'function') {
383+
// if (!this.canDrop(dropTargetProps, monitor)) {
384+
385+
// console.log('dnd-manager: hover:', {...restDropTargetProps, monitorPath: newPath, draggedNode, targetDepth, targetIndex});
386+
387+
this.lastAutoSnapResult = this.autoSnap({dropTargetProps, monitor, targetDepth, targetIndex});
388+
389+
if (!this.lastAutoSnapResult) {
390+
// debugger;
391+
this.autoSnap({dropTargetProps, monitor, targetDepth, targetIndex});
392+
}
393+
394+
console.log('dnd-manager: hover(): lastAutoSnapResult:', this.lastAutoSnapResult);
395+
// }
396+
}
397+
398+
if (this.lastAutoSnapResult) {
399+
({ path, targetIndex, targetDepth} = this.lastAutoSnapResult);
400+
}
283401

284402
// throttle `dragHover` work to available animation frames
285403
cancelAnimationFrame(this.rafId);
286404
this.rafId = requestAnimationFrame(() => {
287405
this.dragHover({
288406
node: draggedNode,
289-
path: monitor.getItem().path,
407+
path,
290408
minimumTreeIndex: targetIndex,
291409
// minimumTreeIndex: dropTargetProps.listIndex,
292410
depth: targetDepth,

‎stories/auto-snap-to-level.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/* eslint-disable react/no-multi-comp */
2+
import PropTypes from 'prop-types';
3+
import React, { Component } from 'react';
4+
import { DndProvider, DragSource } from 'react-dnd';
5+
import { HTML5Backend } from 'react-dnd-html5-backend';
6+
import { SortableTreeWithoutDndContext as SortableTree } from '../src';
7+
// In your own app, you would need to use import styles once in the app
8+
// import 'react-sortable-tree/styles.css';
9+
10+
// -------------------------
11+
// Create an drag source component that can be dragged into the tree
12+
// https://react-dnd.github.io/react-dnd/docs-drag-source.html
13+
// -------------------------
14+
// This type must be assigned to the tree via the `dndType` prop as well
15+
const externalNodeType = 'yourNodeType';
16+
const externalNodeSpec = {
17+
// This needs to return an object with a property `node` in it.
18+
// Object rest spread is recommended to avoid side effects of
19+
// referencing the same object in different trees.
20+
beginDrag: componentProps => ({ node: { ...componentProps.node } }),
21+
};
22+
const externalNodeCollect = (connect /* , monitor */) => ({
23+
connectDragSource: connect.dragSource(),
24+
// Add props via react-dnd APIs to enable more visual
25+
// customization of your component
26+
// isDragging: monitor.isDragging(),
27+
// didDrop: monitor.didDrop(),
28+
});
29+
class externalNodeBaseComponent extends Component {
30+
render() {
31+
const { connectDragSource, node } = this.props;
32+
33+
return connectDragSource(
34+
<div
35+
style={{
36+
display: 'inline-block',
37+
padding: '3px 5px',
38+
background: 'blue',
39+
color: 'white',
40+
}}
41+
>
42+
{node.title}
43+
</div>,
44+
{ dropEffect: 'copy' }
45+
);
46+
}
47+
}
48+
externalNodeBaseComponent.propTypes = {
49+
node: PropTypes.shape({ title: PropTypes.string }).isRequired,
50+
connectDragSource: PropTypes.func.isRequired,
51+
};
52+
const YourExternalNodeComponent = DragSource(
53+
externalNodeType,
54+
externalNodeSpec,
55+
externalNodeCollect
56+
)(externalNodeBaseComponent);
57+
58+
function canDrop(args) {
59+
// debugger;
60+
// console.log('canDrop:', args);
61+
62+
const { node, nextParent } = args;
63+
64+
if (node.notAllowed) {
65+
return false;
66+
}
67+
68+
if (node.isPerson) {
69+
return nextParent && !nextParent.isPerson;
70+
}
71+
72+
return !nextParent;
73+
}
74+
75+
class App extends Component {
76+
constructor(props) {
77+
super(props);
78+
79+
this.state = {
80+
treeData: [
81+
{
82+
title: 'Managers',
83+
id: 1000,
84+
expanded: true,
85+
children: [
86+
{
87+
id: 1,
88+
title: 'Rob',
89+
children: [],
90+
isPerson: true,
91+
},
92+
{
93+
id: 2,
94+
title: 'Joe',
95+
children: [],
96+
isPerson: true,
97+
},
98+
],
99+
},
100+
{
101+
title: 'Clerks',
102+
id: 2000,
103+
expanded: true,
104+
children: [
105+
{
106+
id: 3,
107+
title: 'Bertha',
108+
children: [],
109+
isPerson: true,
110+
},
111+
{
112+
id: 4,
113+
title: 'Billy',
114+
children: [],
115+
isPerson: true,
116+
},
117+
],
118+
},
119+
],
120+
};
121+
122+
this.onChange = this.onChange.bind(this);
123+
124+
this.refReactVirtualizedList = React.createRef();
125+
126+
this.reactVirtualizedListProps = {
127+
// autoHeight: true,
128+
ref: this.refReactVirtualizedList,
129+
}
130+
}
131+
132+
onChange(newTreeData) {
133+
this.setState({treeData: newTreeData});
134+
}
135+
136+
render() {
137+
console.log('render');
138+
139+
return (
140+
<DndProvider backend={HTML5Backend} debugMode>
141+
<div>
142+
<div style={{ height: 500 }}>
143+
<SortableTree
144+
canDrop={canDrop}
145+
dndType={externalNodeType}
146+
isVirtualized={this.props.isVirtualized}
147+
// onDragStateChanged={this.onDragStateChanged}
148+
reactVirtualizedListProps={this.reactVirtualizedListProps}
149+
onChange={this.onChange}
150+
treeData={this.state.treeData}
151+
autoSnapToLevel
152+
/>
153+
</div>
154+
<YourExternalNodeComponent node={{ title: 'Joy division' }} />&nbsp;
155+
<YourExternalNodeComponent node={{ title: 'New worker', isPerson: true, }} />&nbsp;
156+
<YourExternalNodeComponent node={{ title: 'Not allowed', notAllowed: true }} />← drag
157+
this
158+
</div>
159+
</DndProvider>
160+
);
161+
}
162+
}
163+
164+
App.propTypes = {
165+
isVirtualized: PropTypes.bool.isRequired,
166+
}
167+
168+
export default App;

‎stories/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import TouchSupportExample from './touch-support';
2121
import TreeDataIOExample from './tree-data-io';
2222
import TreeToTreeExample from './tree-to-tree';
2323
import RowHeight from "./row-height";
24+
import AutoSnapToLevel from "./auto-snap-to-level";
2425

2526
storiesOf('Basics', module)
2627
.add('Minimal implementation', () => <BarebonesExample />)
@@ -45,4 +46,5 @@ storiesOf('Advanced', module)
4546
<BarebonesExampleNoContext />
4647
))
4748
.add('Row height function', () => <RowHeight isVirtualized={false} />)
48-
.add('Row height function + react-virtualized', () => <RowHeight isVirtualized />);
49+
.add('Row height function + react-virtualized', () => <RowHeight isVirtualized />)
50+
.add('Auto snap to level', () => <AutoSnapToLevel />);

0 commit comments

Comments
 (0)
Please sign in to comment.