Skip to content

Commit 821c4cd

Browse files
committed
Add arrow pointing to dragged row destination if it is not hovered over directly
1 parent 991d2ca commit 821c4cd

File tree

3 files changed

+200
-10
lines changed

3 files changed

+200
-10
lines changed

src/react-sortable-tree.js

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ class ReactSortableTree extends Component {
5959

6060
this.state = {
6161
draggingTreeData: null,
62+
swapFrom: null,
63+
swapLength: null,
64+
swapDepth: null,
6265
rows: this.getRows(props.treeData),
6366
};
6467

@@ -79,6 +82,9 @@ class ReactSortableTree extends Component {
7982
// Calculate the rows to be shown from the new tree data
8083
this.setState({
8184
draggingTreeData: null,
85+
swapFrom: null,
86+
swapLength: null,
87+
swapDepth: null,
8288
rows: this.getRows(nextProps.treeData),
8389
});
8490
}
@@ -113,15 +119,17 @@ class ReactSortableTree extends Component {
113119
expandParent: true,
114120
});
115121

116-
const rows = this.getRows(addedResult.treeData);
122+
const rows = this.getRows(addedResult.treeData);
117123
const expandedParentPath = rows[addedResult.treeIndex].path;
124+
125+
const swapFrom = addedResult.treeIndex;
126+
const swapTo = minimumTreeIndex;
127+
const swapLength = 1 + getDescendantCount({ node: draggedNode });
118128
this.setState({
119-
rows: swapRows(
120-
rows,
121-
addedResult.treeIndex,
122-
minimumTreeIndex,
123-
1 + getDescendantCount({ node: draggedNode })
124-
),
129+
rows: swapRows(rows, swapFrom, swapTo, swapLength),
130+
swapFrom,
131+
swapLength,
132+
swapDepth: depth,
125133
draggingTreeData: changeNodeAtPath({
126134
treeData: this.state.draggingTreeData,
127135
path: expandedParentPath.slice(0, -1),
@@ -135,6 +143,9 @@ class ReactSortableTree extends Component {
135143
if (!dropResult) {
136144
return this.setState({
137145
draggingTreeData: null,
146+
swapFrom: null,
147+
swapLength: null,
148+
swapDepth: null,
138149
rows: this.getRows(this.props.treeData),
139150
});
140151
}
@@ -238,6 +249,9 @@ class ReactSortableTree extends Component {
238249
path={path}
239250
lowerSiblingCounts={lowerSiblingCounts}
240251
scaffoldBlockPxWidth={this.props.scaffoldBlockPxWidth}
252+
swapFrom={this.state.swapFrom}
253+
swapLength={this.state.swapLength}
254+
swapDepth={this.state.swapDepth}
241255
maxDepth={this.props.maxDepth}
242256
dragHover={this.dragHover}
243257
>

src/tree-node.js

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@ import styles from './tree-node.scss';
55
const TreeNode = ({
66
children,
77
listIndex,
8+
swapFrom,
9+
swapLength,
10+
swapDepth,
811
scaffoldBlockPxWidth,
912
lowerSiblingCounts,
1013
connectDropTarget,
1114
isOver,
1215
draggedNode,
1316
canDrop,
14-
treeIndex: _treeIndex, // Delete from otherProps
17+
treeIndex,
1518
getPrevRow: _getPrevRow, // Delete from otherProps
1619
node: _node, // Delete from otherProps
1720
path: _path, // Delete from otherProps
@@ -21,7 +24,8 @@ const TreeNode = ({
2124
}) => {
2225
// Construct the scaffold representing the structure of the tree
2326
const scaffoldBlockCount = lowerSiblingCounts.length;
24-
const scaffold = lowerSiblingCounts.map((lowerSiblingCount, i) => {
27+
const scaffold = [];
28+
lowerSiblingCounts.forEach((lowerSiblingCount, i) => {
2529
let lineClass = '';
2630
if (lowerSiblingCount > 0) {
2731
// At this level in the tree, the nodes had sibling nodes further down
@@ -69,13 +73,49 @@ const TreeNode = ({
6973
lineClass = `${styles.lineHalfVerticalTop} ${styles.lineHalfHorizontalRight}`;
7074
}
7175

72-
return (
76+
scaffold.push(
7377
<div
7478
key={`pre_${i}`}
7579
style={{ width: scaffoldBlockPxWidth }}
7680
className={`${styles.lineBlock} ${lineClass}`}
7781
/>
7882
);
83+
84+
if (treeIndex !== listIndex) {
85+
// This row has been shifted
86+
let highlightLineClass = '';
87+
if (i === swapDepth) {
88+
// This block is at the depth of the line pointing to the new destination
89+
90+
if (listIndex === swapFrom + swapLength - 1) {
91+
// This block is on the bottom (target) line
92+
highlightLineClass = styles.highlightBottomLeftCorner;
93+
} else if (treeIndex === swapFrom) {
94+
// This block is on the top (source) line
95+
highlightLineClass = styles.highlightTopLeftCorner;
96+
} else {
97+
// This block is between the bottom and top
98+
highlightLineClass = styles.highlightLineVertical;
99+
}
100+
} else if (i === swapDepth + 1 && listIndex === swapFrom + swapLength - 1) {
101+
// This block points at the target block (where the row will go when released)
102+
highlightLineClass = styles.highlightArrow;
103+
}
104+
105+
// Add the highlight line block if it met one of the conditions above
106+
if (highlightLineClass) {
107+
scaffold.push(
108+
<div
109+
key={`highlight_${i}`}
110+
style={{
111+
width: scaffoldBlockPxWidth,
112+
left: scaffoldBlockPxWidth * i,
113+
}}
114+
className={`${styles.absoluteLineBlock} ${highlightLineClass}`}
115+
/>
116+
);
117+
}
118+
}
79119
});
80120

81121
return connectDropTarget(
@@ -103,6 +143,9 @@ TreeNode.propTypes = {
103143
treeIndex: PropTypes.number.isRequired,
104144
node: PropTypes.object.isRequired,
105145
path: PropTypes.arrayOf(PropTypes.oneOfType([ PropTypes.string, PropTypes.number ])).isRequired,
146+
swapFrom: PropTypes.number,
147+
swapDepth: PropTypes.number,
148+
swapLength: PropTypes.number,
106149
scaffoldBlockPxWidth: PropTypes.number.isRequired,
107150
lowerSiblingCounts: PropTypes.array.isRequired,
108151

src/tree-node.scss

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@
2020
display: inline-block;
2121
}
2222

23+
.absoluteLineBlock {
24+
@extend .lineBlock;
25+
position: absolute;
26+
top: 0;
27+
}
28+
2329
%blackLineBase {
2430
position: absolute;
2531
content: '';
@@ -92,3 +98,130 @@
9298
bottom: 0;
9399
}
94100
}
101+
102+
/* Highlight line for pointing to dragged row destination
103+
========================================================================== */
104+
$highlight-color: #36C2F6;
105+
$highlight-line-size: 8px; // Make it an even number for clean rendering
106+
107+
%highlightLineBase {
108+
position: absolute;
109+
content: '';
110+
background-color: $highlight-color;
111+
z-index: 3;
112+
}
113+
114+
/**
115+
* +-----+
116+
* | |
117+
* | |
118+
* | |
119+
* ======>
120+
*/
121+
.highlightArrow {
122+
$arrow-size: 18px;
123+
z-index: 3;
124+
125+
&::before {
126+
content: '';
127+
position: absolute;
128+
border-bottom: solid $highlight-line-size $highlight-color;
129+
height: calc(100% - #{$highlight-line-size / 2});
130+
top: 0;
131+
right: $arrow-size;
132+
width: calc(100% - #{$arrow-size});
133+
}
134+
135+
&::after {
136+
content: '';
137+
position: absolute;
138+
height: 0;
139+
right: 0;
140+
top: 100%;
141+
margin-top: -1 * $arrow-size / 1.46 ;
142+
border-top: $arrow-size / 1.4 solid transparent;
143+
border-bottom: $arrow-size / 1.4 solid transparent;
144+
border-left: $arrow-size solid $highlight-color;
145+
}
146+
}
147+
148+
/**
149+
* +--+--+
150+
* | | |
151+
* | | |
152+
* | | |
153+
* +--+--+
154+
*/
155+
.highlightLineVertical {
156+
&::before {
157+
@extend %highlightLineBase;
158+
width: $highlight-line-size;
159+
margin-left: $highlight-line-size / -2;
160+
left: 50%;
161+
top: 0;
162+
height: 100%;
163+
}
164+
165+
@keyframes arrow-pulse {
166+
$base-multiplier: 10;
167+
0% {
168+
transform: translate(0, 0);
169+
opacity: 0;
170+
}
171+
30% {
172+
transform: translate(0, 30% * $base-multiplier);
173+
opacity: 1;
174+
}
175+
70% {
176+
transform: translate(0, 70% * $base-multiplier);
177+
opacity: 1;
178+
}
179+
100% {
180+
transform: translate(0, 100% * $base-multiplier);
181+
opacity: 0;
182+
}
183+
}
184+
185+
&::after {
186+
z-index: 3;
187+
content: '';
188+
position: absolute;
189+
height: 0;
190+
margin-left: -1 * $highlight-line-size / 2;
191+
left: 50%;
192+
top: 0;
193+
border-left: $highlight-line-size / 2 solid transparent;
194+
border-right: $highlight-line-size / 2 solid transparent;
195+
border-top: $highlight-line-size / 2 solid white;
196+
animation: arrow-pulse 1s infinite linear both;
197+
}
198+
}
199+
200+
.highlightTopLeftCorner {
201+
&::before {
202+
z-index: 3;
203+
content: '';
204+
position: absolute;
205+
border-top: solid $highlight-line-size $highlight-color;
206+
border-left: solid $highlight-line-size $highlight-color;
207+
height: calc(50% - #{$highlight-line-size / 2});
208+
top: 50%;
209+
margin-top: $highlight-line-size / -2;
210+
right: 0;
211+
width: calc(50% - #{$highlight-line-size / 2});
212+
}
213+
}
214+
215+
.highlightBottomLeftCorner {
216+
&::before {
217+
z-index: 3;
218+
content: '';
219+
position: absolute;
220+
border-bottom: solid $highlight-line-size $highlight-color;
221+
border-left: solid $highlight-line-size $highlight-color;
222+
height: calc(100% - #{$highlight-line-size / 2});
223+
top: 0;
224+
right: 0;
225+
width: calc(50% - #{$highlight-line-size / 2});
226+
}
227+
}

0 commit comments

Comments
 (0)