Skip to content

Commit 495a08d

Browse files
committed
Scroll container when dragging near edges
1 parent e81182a commit 495a08d

File tree

4 files changed

+123
-11
lines changed

4 files changed

+123
-11
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ onMoveNode | func | | | Ca
5757
onVisibilityToggle | func | | | Called after children nodes collapsed or expanded. <div>`({ treeData: object[], node: object, expanded: bool }): void`</div>
5858
reactVirtualizedListProps | object | | | Custom properties to hand to the [react-virtualized list](https://github.com/bvaughn/react-virtualized/blob/master/docs/List.md#prop-types)
5959
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`
60+
slideRegionSize | number | `100` | | Size in px of the region near the edges that initiates scrolling on dragover.
6061
scaffoldBlockPxWidth | number | `44` | | The width of the blocks containing the lines representing the structure of the tree.
6162
nodeContentRenderer | any | NodeRendererDefault | | Override the default component for rendering nodes (but keep the scaffolding generator) This is an advanced option for complete customization of the appearance. It is best to copy the component in `node-renderer-default.js` to use as a base, and customize as needed.
6263

src/react-sortable-tree.js

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class ReactSortableTree extends Component {
4646
rows: this.getRows(props.treeData),
4747
searchMatches: [],
4848
searchFocusTreeIndex: null,
49+
scrollToPixel: null,
4950
};
5051

5152
this.toggleChildrenVisibility = this.toggleChildrenVisibility.bind(this);
@@ -286,17 +287,34 @@ class ReactSortableTree extends Component {
286287
});
287288
}
288289

290+
scrollBy(x, y) {
291+
if (!this.containerRef) {
292+
return;
293+
}
294+
295+
if (x !== 0) {
296+
this.containerRef.getElementsByClassName(styles.virtualScrollOverride)[0].scrollLeft += x;
297+
}
298+
299+
if (y !== 0) {
300+
this.scrollTop = this.scrollTop ? (this.scrollTop + y) : y;
301+
this.setState({ scrollToPixel: this.scrollTop });
302+
}
303+
}
304+
289305
render() {
290306
const {
291307
style,
292308
className,
293309
innerStyle,
294310
rowHeight,
311+
_connectDropTarget,
295312
} = this.props;
296313
const {
297314
rows,
298315
searchMatches,
299316
searchFocusTreeIndex,
317+
scrollToPixel,
300318
} = this.state;
301319

302320
// Get indices for rows that match the search conditions
@@ -306,10 +324,11 @@ class ReactSortableTree extends Component {
306324
// Seek to the focused search result if there is one specified
307325
const scrollToInfo = searchFocusTreeIndex !== null ? { scrollToIndex: searchFocusTreeIndex } : {};
308326

309-
return (
327+
return _connectDropTarget(
310328
<div
311329
className={styles.tree + (className ? ` ${className}` : '')}
312330
style={{ height: '100%', ...style }}
331+
ref={(el) => { this.containerRef = el; }}
313332
>
314333
<AutoSizer>
315334
{({height, width}) => (
@@ -318,6 +337,8 @@ class ReactSortableTree extends Component {
318337
scrollToAlignment="start"
319338
className={styles.virtualScrollOverride}
320339
width={width}
340+
scrollTop={scrollToPixel}
341+
onScroll={({ scrollTop }) => { this.scrollTop = scrollTop; }}
321342
height={height}
322343
style={innerStyle}
323344
rowCount={rows.length}
@@ -412,6 +433,9 @@ ReactSortableTree.propTypes = {
412433
// height of a row given its index: `({ index: number }): number`
413434
rowHeight: PropTypes.oneOfType([ PropTypes.number, PropTypes.func ]),
414435

436+
// Size in px of the region near the edges that initiates scrolling on dragover
437+
slideRegionSize: PropTypes.number.isRequired, // eslint-disable-line react/no-unused-prop-types
438+
415439
// Custom properties to hand to the react-virtualized list
416440
// https://github.com/bvaughn/react-virtualized/blob/master/docs/List.md#prop-types
417441
reactVirtualizedListProps: PropTypes.object,
@@ -463,12 +487,16 @@ ReactSortableTree.propTypes = {
463487

464488
// Called after children nodes collapsed or expanded.
465489
onVisibilityToggle: PropTypes.func,
490+
491+
// Injected by react-dnd
492+
_connectDropTarget: PropTypes.func.isRequired,
466493
};
467494

468495
ReactSortableTree.defaultProps = {
469496
getNodeKey: defaultGetNodeKey,
470497
nodeContentRenderer: NodeRendererDefault,
471498
rowHeight: 62,
499+
slideRegionSize: 100,
472500
scaffoldBlockPxWidth: 44,
473501
style: {},
474502
innerStyle: {},

src/react-sortable-tree.scss

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
* Extra class applied to VirtualScroll through className prop
1010
*/
1111
.virtualScrollOverride {
12-
overflow-x: visible !important;
13-
overflow-y: auto !important;
14-
1512
* {
1613
box-sizing: border-box;
1714
}

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

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
getDepth,
1010
} from './tree-data-utils';
1111

12-
const myDragSource = {
12+
const nodeDragSource = {
1313
beginDrag(props) {
1414
props.startDrag(props);
1515

@@ -82,7 +82,7 @@ function canDrop(dropTargetProps, monitor, isHover = false) {
8282
);
8383
}
8484

85-
const myDropTarget = {
85+
const nodeDropTarget = {
8686
drop(dropTargetProps, monitor) {
8787
return {
8888
node: monitor.getItem().node,
@@ -108,15 +108,93 @@ const myDropTarget = {
108108
canDrop,
109109
};
110110

111-
function dragSourcePropInjection(connect, monitor) {
111+
const scrollDropTarget = {
112+
hover(props, monitor, component) {
113+
const cancelAnimationFrame = window.cancelAnimationFrame || (timeout => clearTimeout(timeout));
114+
const requestAnimationFrame = window.requestAnimationFrame || (func => setTimeout(func, 1000 / 60));
115+
116+
// If already scrolling, stop the previous scroll loop
117+
if (this.lastScroll) {
118+
cancelAnimationFrame(this.lastScroll);
119+
this.lastScroll = null;
120+
clearTimeout(this.removeTimeout);
121+
}
122+
123+
const slideRegionSize = component.props.slideRegionSize;
124+
const { x: dragXOffset, y: dragYOffset } = monitor.getClientOffset();
125+
const {
126+
top: containerTop,
127+
bottom: containerBottom,
128+
left: containerLeft,
129+
right: containerRight,
130+
} = component.containerRef.getBoundingClientRect();
131+
let yScrollDirection = 0;
132+
let yScrollMagnitude = 0;
133+
const fromTop = dragYOffset - slideRegionSize - Math.max(containerTop, 0);
134+
if (fromTop <= 0) {
135+
// Move up
136+
yScrollDirection = -1;
137+
yScrollMagnitude = Math.sqrt(-1 * fromTop);
138+
} else {
139+
const fromBottom = dragYOffset + slideRegionSize - Math.min(containerBottom, window.innerHeight);
140+
if (fromBottom >= 0) {
141+
// Move down
142+
yScrollDirection = 1;
143+
yScrollMagnitude = Math.sqrt(fromBottom);
144+
}
145+
}
146+
147+
let xScrollDirection = 0;
148+
let xScrollMagnitude = 0;
149+
const fromLeft = dragXOffset - slideRegionSize - Math.max(containerLeft, 0);
150+
if (fromLeft <= 0) {
151+
// Move up
152+
xScrollDirection = -1;
153+
xScrollMagnitude = Math.ceil(Math.sqrt(-1 * fromLeft));
154+
} else {
155+
const fromRight = dragXOffset + slideRegionSize - Math.min(containerRight, window.innerWidth);
156+
if (fromRight >= 0) {
157+
// Move down
158+
xScrollDirection = 1;
159+
xScrollMagnitude = Math.ceil(Math.sqrt(fromRight));
160+
}
161+
}
162+
163+
// Don't do anything if there is no scroll operation
164+
if (xScrollDirection === 0 && yScrollDirection === 0) {
165+
return;
166+
}
167+
168+
// Indefinitely scrolls the container at a constant rate
169+
const doScroll = () => {
170+
component.scrollBy(xScrollDirection * xScrollMagnitude, yScrollDirection * yScrollMagnitude);
171+
this.lastScroll = requestAnimationFrame(doScroll);
172+
};
173+
174+
// Stop the scroll loop after a period of inactivity
175+
this.removeTimeout = setTimeout(() => {
176+
cancelAnimationFrame(this.lastScroll);
177+
this.lastScroll = null;
178+
}, 20);
179+
180+
// Start the scroll loop
181+
this.lastScroll = requestAnimationFrame(doScroll);
182+
},
183+
184+
canDrop() {
185+
return false;
186+
},
187+
};
188+
189+
function nodeDragSourcePropInjection(connect, monitor) {
112190
return {
113191
connectDragSource: connect.dragSource(),
114192
connectDragPreview: connect.dragPreview(),
115193
isDragging: monitor.isDragging(),
116194
};
117195
}
118196

119-
function dropTargetPropInjection(connect, monitor) {
197+
function nodeDropTargetPropInjection(connect, monitor) {
120198
const dragged = monitor.getItem();
121199
return {
122200
connectDropTarget: connect.dropTarget(),
@@ -126,14 +204,22 @@ function dropTargetPropInjection(connect, monitor) {
126204
};
127205
}
128206

207+
function scrollDropTargetPropInjection(connect) {
208+
return {
209+
_connectDropTarget: connect.dropTarget(),
210+
};
211+
}
212+
129213
export function dndWrapSource(el) {
130-
return dragSource(ItemTypes.HANDLE, myDragSource, dragSourcePropInjection)(el);
214+
return dragSource(ItemTypes.HANDLE, nodeDragSource, nodeDragSourcePropInjection)(el);
131215
}
132216

133217
export function dndWrapTarget(el) {
134-
return dropTarget(ItemTypes.HANDLE, myDropTarget, dropTargetPropInjection)(el);
218+
return dropTarget(ItemTypes.HANDLE, nodeDropTarget, nodeDropTargetPropInjection)(el);
135219
}
136220

137221
export function dndWrapRoot(el) {
138-
return dragDropContext(HTML5Backend)(el);
222+
return dragDropContext(HTML5Backend)(
223+
dropTarget(ItemTypes.HANDLE, scrollDropTarget, scrollDropTargetPropInjection)(el)
224+
);
139225
}

0 commit comments

Comments
 (0)