From 336488354db649950c7d171d68573687a840babc Mon Sep 17 00:00:00 2001 From: komarov Date: Fri, 27 Jan 2023 09:24:23 +0300 Subject: [PATCH] JSK-11518: autorecalc rows heights while dragging node, #264 --- package.json | 3 +- src/react-sortable-tree.js | 164 +++++++++++++++++++++++++++---------- src/utils/dnd-manager.js | 33 ++++++-- stories/row-height.js | 80 +++--------------- 4 files changed, 160 insertions(+), 120 deletions(-) diff --git a/package.json b/package.json index 5b498eed..d2d2643f 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/react-sortable-tree.js b/src/react-sortable-tree.js index 16eca0e2..f39e0e0e 100644 --- a/src/react-sortable-tree.js +++ b/src/react-sortable-tree.js @@ -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'; @@ -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); @@ -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 ); @@ -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); } @@ -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 = {}; @@ -195,7 +248,7 @@ class ReactSortableTree extends Component { instanceProps.searchQuery = nextProps.searchQuery; instanceProps.searchFocusOffset = nextProps.searchFocusOffset; newState.instanceProps = {...instanceProps, ...newState.instanceProps }; - + return newState; } @@ -210,6 +263,10 @@ class ReactSortableTree extends Component { }); } } + + if (this.props.treeData !== prevProps.treeData) { + this.rowHeightsRecomputeRequired(); + } } componentWillUnmount() { @@ -357,6 +414,8 @@ class ReactSortableTree extends Component { } startDrag({ path }) { + this.firstRenderAfterDragStart = true; + this.setState(prevState => { const { treeData: draggingTreeData, @@ -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, @@ -427,6 +493,9 @@ class ReactSortableTree extends Component { } endDrag(dropResult) { + + console.log('react-sortable-tree: endDrag(): dropResult:', dropResult); + const { instanceProps } = this.state; const resetTree = () => @@ -479,10 +548,13 @@ class ReactSortableTree extends Component { prevTreeIndex: treeIndex, }); } + + this.rowHeightsRecomputeRequired(); } drop(dropResult) { this.moveNode(dropResult); + this.rowHeightsRecomputeRequired(); } canNodeHaveChildren(node) { @@ -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]; @@ -620,7 +692,7 @@ class ReactSortableTree extends Component { reactVirtualizedListProps, getNodeKey, rowDirection, - } = mergeTheme(this.props); + } = this.mergeTheme(this.props); const { searchMatches, searchFocusTreeIndex, @@ -712,6 +784,7 @@ class ReactSortableTree extends Component { ? rowHeight : ({ index }) => rowHeight({ + draggedNode, index, treeIndex: index, node: rows[index].node, @@ -744,6 +817,7 @@ class ReactSortableTree extends Component { typeof rowHeight !== 'function' ? rowHeight : rowHeight({ + draggedNode: this.firstRenderAfterDragStart ? null : draggedNode , index, treeIndex: index, node: row.node, @@ -759,6 +833,8 @@ class ReactSortableTree extends Component { ); } + this.firstRenderAfterDragStart = false; + return (
{ + this.lastHoverClientOffset = null; + this.startDrag(props); return { @@ -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 @@ -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; @@ -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; @@ -261,6 +275,8 @@ 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(() => { @@ -268,6 +284,7 @@ export default class DndManager { node: draggedNode, path: monitor.getItem().path, minimumTreeIndex: targetIndex, + // minimumTreeIndex: dropTargetProps.listIndex, depth: targetDepth, }); }); diff --git a/stories/row-height.js b/stories/row-height.js index 98dffee9..9964b86d 100644 --- a/stories/row-height.js +++ b/stories/row-height.js @@ -56,14 +56,23 @@ const YourExternalNodeComponent = DragSource( )(externalNodeBaseComponent); function canDrop(args) { - console.log('canDrop:', args); - const { node, nextParent } = args; if (node.isPerson) { return nextParent && !nextParent.isPerson; } - return true; + return !nextParent; +} + + +function calcRowHeight(args) { + console.log('calcRowHeight:', args); + + if (args.node === args.draggedNode) { + return 10; + } + + return 62; } class App extends Component { @@ -113,12 +122,7 @@ class App extends Component { ], }; - this.draggedNode = null; - - this.onDragStateChanged = this.onDragStateChanged.bind(this); - this.calcRowHeight = this.calcRowHeight.bind(this); this.onChange= this.onChange.bind(this); - this.virtualListRecomputeRowHeights = this.virtualListRecomputeRowHeights.bind(this); this.refReactVirtualizedList = React.createRef(); @@ -126,70 +130,13 @@ class App extends Component { // autoHeight: true, ref: this.refReactVirtualizedList, } - - this.recomputingRowHeight = false; - } - - onDragStateChanged(args) { - console.log('onDragStateChanged:', args); - - const { draggedNode } = args; - const { isVirtualized } = this.props; - this.draggedNode = draggedNode; - - const app = this; - - if (!draggedNode && !isVirtualized) { - setTimeout( - () => { - app.setState((state) => { - console.log('state:', state); - return ({ - treeData: [...state.treeData] - }); - }); - }, - 10 - ); - } - - if (draggedNode) { - this.dragInterval = setInterval(this.virtualListRecomputeRowHeights, 250); - } else { - clearInterval(this.dragInterval); - this.virtualListRecomputeRowHeights(); - } - - // this.virtualListRecomputeRowHeights(); - } - - calcRowHeight(args) { - console.log('calcRowHeight:', args); - - if (args.node === this.draggedNode) { - return 10; - } - - return 62; } onChange(newTreeData) { this.setState({treeData: newTreeData}); } - virtualListRecomputeRowHeights() { - if (this.props.isVirtualized && !this.recomputingRowHeight) { - this.recomputingRowHeight = true; - console.log('calling recomputeRowHeights()'); - // TODO seems like calling recomputeRowHeights() aborts dragging :c - this.refReactVirtualizedList.current.wrappedInstance.current.recomputeRowHeights(); - this.recomputingRowHeight = false; - } - } - render() { - console.log('render'); - return (
@@ -198,10 +145,9 @@ class App extends Component { canDrop={canDrop} dndType={externalNodeType} isVirtualized={this.props.isVirtualized} - onDragStateChanged={this.onDragStateChanged} reactVirtualizedListProps={this.reactVirtualizedListProps} - rowHeight={this.calcRowHeight} onChange={this.onChange} + rowHeight={calcRowHeight} treeData={this.state.treeData} />