Skip to content

Commit

Permalink
JSK-11518: autorecalc rows heights while dragging node, frontend-coll…
Browse files Browse the repository at this point in the history
  • Loading branch information
d9k committed Feb 3, 2023
1 parent 42ddf3e commit 3364883
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 120 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@uniweb/react-sortable-tree",
"version": "2.8.2",
"version": "2.8.4",
"description": "Drag-and-drop sortable component for nested data and hierarchies",
"scripts": {
"prebuild": "yarn run lint && yarn run clean",
Expand Down Expand Up @@ -67,6 +67,7 @@
],
"dependencies": {
"frontend-collective-react-dnd-scrollzone": "^1.0.2",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"prop-types": "^15.6.1",
"react-dnd": "^11.1.3",
Expand Down
164 changes: 120 additions & 44 deletions src/react-sortable-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import withScrolling, {
createVerticalStrength,
} from 'frontend-collective-react-dnd-scrollzone';
import isEqual from 'lodash.isequal';
import debounce from 'lodash.debounce';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { DndContext, DndProvider } from 'react-dnd';
Expand Down Expand Up @@ -39,51 +40,21 @@ import {

let treeIdCounter = 1;

const mergeTheme = props => {
const merged = {
...props,
style: { ...props.theme.style, ...props.style },
innerStyle: { ...props.theme.innerStyle, ...props.innerStyle },
reactVirtualizedListProps: {
...props.theme.reactVirtualizedListProps,
...props.reactVirtualizedListProps,
},
};

const overridableDefaults = {
nodeContentRenderer: NodeRendererDefault,
placeholderRenderer: PlaceholderRendererDefault,
rowHeight: 62,
scaffoldBlockPxWidth: 44,
slideRegionSize: 100,
treeNodeRenderer: TreeNode,
};
Object.keys(overridableDefaults).forEach(propKey => {
// If prop has been specified, do not change it
// If prop is specified in theme, use the theme setting
// If all else fails, fall back to the default
if (props[propKey] === null) {
merged[propKey] =
typeof props.theme[propKey] !== 'undefined'
? props.theme[propKey]
: overridableDefaults[propKey];
}
});

return merged;
};

class ReactSortableTree extends Component {
constructor(props) {
super(props);

this.refReactVirtualizedList = React.createRef();
this.rowHeightRecomputing = false;
this.rowHeightRerunPlanned = false;

const {
dndType,
nodeContentRenderer,
treeNodeRenderer,
isVirtualized,
slideRegionSize,
} = mergeTheme(props);
} = this.mergeTheme(props);

this.dndManager = new DndManager(this);

Expand All @@ -92,6 +63,7 @@ class ReactSortableTree extends Component {
treeIdCounter += 1;
this.dndType = dndType || this.treeId;
this.nodeContentRenderer = this.dndManager.wrapSource(nodeContentRenderer);
this.firstRenderAfterDragStart = true;
this.treePlaceholderRenderer = this.dndManager.wrapPlaceholder(
TreePlaceholder
);
Expand Down Expand Up @@ -130,6 +102,10 @@ class ReactSortableTree extends Component {
this.dragHover = this.dragHover.bind(this);
this.endDrag = this.endDrag.bind(this);
this.drop = this.drop.bind(this);
this.rowHeightsVirtualListRecompute = this.rowHeightsVirtualListRecompute.bind(this);
this.rowHeightsVirtualListRecomputeRerunAfterDone = this.rowHeightsVirtualListRecomputeRerunAfterDone.bind(this);
this.rowHeightsVirtualListRecomputeRerunAfterDoneDebounced = debounce(this.rowHeightsVirtualListRecomputeRerunAfterDone, 100).bind(this);
this.rowHeightsRecomputeRequired = this.rowHeightsRecomputeRequired.bind(this);
this.handleDndMonitorChange = this.handleDndMonitorChange.bind(this);
}

Expand All @@ -152,6 +128,83 @@ class ReactSortableTree extends Component {
.subscribeToStateChange(this.handleDndMonitorChange);
}

mergeTheme(props) {
const merged = {
...props,
style: { ...props.theme.style, ...props.style },
innerStyle: { ...props.theme.innerStyle, ...props.innerStyle },
reactVirtualizedListProps: {
...props.theme.reactVirtualizedListProps,
...props.reactVirtualizedListProps,
ref: (newRef) => {
// eslint-disable-next-line no-param-reassign
this.refReactVirtualizedList.current = newRef;
const propsVListRef = props.reactVirtualizedListProps.ref;

if (propsVListRef) {
if (typeof propsVListRef === 'function') {
propsVListRef(newRef)
} else {
propsVListRef.current = newRef;
}
}
}
},
};

const overridableDefaults = {
nodeContentRenderer: NodeRendererDefault,
placeholderRenderer: PlaceholderRendererDefault,
rowHeight: 62,
scaffoldBlockPxWidth: 44,
slideRegionSize: 100,
treeNodeRenderer: TreeNode,
};
Object.keys(overridableDefaults).forEach(propKey => {
// If prop has been specified, do not change it
// If prop is specified in theme, use the theme setting
// If all else fails, fall back to the default
if (props[propKey] === null) {
merged[propKey] =
typeof props.theme[propKey] !== 'undefined'
? props.theme[propKey]
: overridableDefaults[propKey];
}
});

return merged;
};

rowHeightsVirtualListRecompute() {
if (this.props.isVirtualized) {
if (!this.rowHeightRecomputing) {
this.rowHeightRecomputing = true;

// TODO seems like calling recomputeRowHeights() immediately aborts dragging :c
this.refReactVirtualizedList.current.wrappedInstance.current.recomputeRowHeights();
this.rowHeightRecomputing = false;
if (this.rowHeightRerunPlanned) {
this.rowHeightRerunPlanned = false;
this.rowHeightsVirtualListRecompute();
}
}
} else {
// this.forceUpdate();
}
}

rowHeightsVirtualListRecomputeRerunAfterDone() {
if (this.rowHeightRecomputing) {
this.rowHeightRerunPlanned = true;
} else {
this.rowHeightsVirtualListRecompute();
}
}

rowHeightsRecomputeRequired() {
this.rowHeightsVirtualListRecomputeRerunAfterDoneDebounced();
}

static getDerivedStateFromProps(nextProps, prevState) {
const { instanceProps } = prevState;
const newState = {};
Expand Down Expand Up @@ -195,7 +248,7 @@ class ReactSortableTree extends Component {
instanceProps.searchQuery = nextProps.searchQuery;
instanceProps.searchFocusOffset = nextProps.searchFocusOffset;
newState.instanceProps = {...instanceProps, ...newState.instanceProps };

return newState;
}

Expand All @@ -210,6 +263,10 @@ class ReactSortableTree extends Component {
});
}
}

if (this.props.treeData !== prevProps.treeData) {
this.rowHeightsRecomputeRequired();
}
}

componentWillUnmount() {
Expand Down Expand Up @@ -357,6 +414,8 @@ class ReactSortableTree extends Component {
}

startDrag({ path }) {
this.firstRenderAfterDragStart = true;

this.setState(prevState => {
const {
treeData: draggingTreeData,
Expand Down Expand Up @@ -408,16 +467,23 @@ class ReactSortableTree extends Component {
const rows = this.getRows(addedResult.treeData);
const expandedParentPath = rows[addedResult.treeIndex].path;

const changeArgs = {
treeData: newDraggingTreeData,
path: expandedParentPath.slice(0, -1),
newNode: ({ node }) => ({ ...node, expanded: true }),
getNodeKey: this.props.getNodeKey,
};

// console.log('react-sortable-tree: dragHover(): changeArgs:', changeArgs);
console.log('react-sortable-tree: dragHover(): changeArgs.path:', changeArgs.path);

this.rowHeightsRecomputeRequired();

return {
draggedNode,
draggedDepth,
draggedMinimumTreeIndex,
draggingTreeData: changeNodeAtPath({
treeData: newDraggingTreeData,
path: expandedParentPath.slice(0, -1),
newNode: ({ node }) => ({ ...node, expanded: true }),
getNodeKey: this.props.getNodeKey,
}),
draggingTreeData: changeNodeAtPath(changeArgs),
// reset the scroll focus so it doesn't jump back
// to a search result while dragging
searchFocusTreeIndex: null,
Expand All @@ -427,6 +493,9 @@ class ReactSortableTree extends Component {
}

endDrag(dropResult) {

console.log('react-sortable-tree: endDrag(): dropResult:', dropResult);

const { instanceProps } = this.state;

const resetTree = () =>
Expand Down Expand Up @@ -479,10 +548,13 @@ class ReactSortableTree extends Component {
prevTreeIndex: treeIndex,
});
}

this.rowHeightsRecomputeRequired();
}

drop(dropResult) {
this.moveNode(dropResult);
this.rowHeightsRecomputeRequired();
}

canNodeHaveChildren(node) {
Expand Down Expand Up @@ -552,7 +624,7 @@ class ReactSortableTree extends Component {
scaffoldBlockPxWidth,
searchFocusOffset,
rowDirection,
} = mergeTheme(this.props);
} = this.mergeTheme(this.props);
const TreeNodeRenderer = this.treeNodeRenderer;
const NodeContentRenderer = this.nodeContentRenderer;
const nodeKey = path[path.length - 1];
Expand Down Expand Up @@ -620,7 +692,7 @@ class ReactSortableTree extends Component {
reactVirtualizedListProps,
getNodeKey,
rowDirection,
} = mergeTheme(this.props);
} = this.mergeTheme(this.props);
const {
searchMatches,
searchFocusTreeIndex,
Expand Down Expand Up @@ -712,6 +784,7 @@ class ReactSortableTree extends Component {
? rowHeight
: ({ index }) =>
rowHeight({
draggedNode,
index,
treeIndex: index,
node: rows[index].node,
Expand Down Expand Up @@ -744,6 +817,7 @@ class ReactSortableTree extends Component {
typeof rowHeight !== 'function'
? rowHeight
: rowHeight({
draggedNode: this.firstRenderAfterDragStart ? null : draggedNode ,
index,
treeIndex: index,
node: row.node,
Expand All @@ -759,6 +833,8 @@ class ReactSortableTree extends Component {
);
}

this.firstRenderAfterDragStart = false;

return (
<div
className={classnames('rst__tree', className, rowDirectionClass)}
Expand Down
33 changes: 25 additions & 8 deletions src/utils/dnd-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { memoizedInsertNode } from './memoized-tree-data-utils';
export default class DndManager {
constructor(treeRef) {
this.treeRef = treeRef;
this.lastHoverClientOffset = null;
}

get startDrag() {
Expand Down Expand Up @@ -159,6 +160,8 @@ export default class DndManager {
wrapSource(el) {
const nodeDragSource = {
beginDrag: props => {
this.lastHoverClientOffset = null;

this.startDrag(props);

return {
Expand Down Expand Up @@ -216,9 +219,6 @@ export default class DndManager {
},

hover: (dropTargetProps, monitor, component) => {

// console.log('__TEST_2__ hover:', dropTargetProps, monitor, component);

/**
* fix "Can't drop external dragsource below tree"
* from https://github.com/frontend-collective/react-sortable-tree/issues/483#issuecomment-581139473
Expand All @@ -231,11 +231,25 @@ export default class DndManager {
component
);
const draggedNode = monitor.getItem().node;

// TODO scroll position?
const clientOffset = monitor.getClientOffset();

const needsRedraw =
// Redraw if hovered above different nodes
dropTargetProps.node !== draggedNode ||
// Or hovered above the same node but at a different depth
targetDepth !== dropTargetProps.path.length - 1;
(
// Redraw if hovered above different nodes
dropTargetProps.node !== draggedNode ||
// Or hovered above the same node but at a different depth
targetDepth !== dropTargetProps.path.length - 1
) && (
!this.lastHoverClientOffset ||
(
Math.abs(this.lastHoverClientOffset.x - clientOffset.x) > 0.1 ||
Math.abs(this.lastHoverClientOffset.x - clientOffset.x) > 0.1
)
);

this.lastHoverClientOffset = clientOffset;

if (!needsRedraw) {
return;
Expand All @@ -247,7 +261,7 @@ export default class DndManager {
// Get vertical middle
const hoverMiddleY =
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();

// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;

Expand All @@ -261,13 +275,16 @@ export default class DndManager {
targetIndex = dropTargetProps.treeIndex + 1;
}

// console.log('dnd-manager: hover:', {...dropTargetProps, draggedNode, targetDepth, targetIndex});

// throttle `dragHover` work to available animation frames
cancelAnimationFrame(this.rafId);
this.rafId = requestAnimationFrame(() => {
this.dragHover({
node: draggedNode,
path: monitor.getItem().path,
minimumTreeIndex: targetIndex,
// minimumTreeIndex: dropTargetProps.listIndex,
depth: targetDepth,
});
});
Expand Down
Loading

0 comments on commit 3364883

Please sign in to comment.