Skip to content

Commit

Permalink
feat: Display droppable placeholder element when tree is empty
Browse files Browse the repository at this point in the history
BREAKING CHANGES: Trees that are empty now display a placeholder element
in their place instead of being simply empty.
  • Loading branch information
fritz-c committed Aug 4, 2017
1 parent 561ef88 commit 2cd371c
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ slideRegionSize | number | `100` | | Si
scaffoldBlockPxWidth | number | `44` | | The width of the blocks containing the lines representing the structure of the tree.
isVirtualized | bool | `true` | | Set to false to disable virtualization. __NOTE__: Auto-scrolling while dragging, and scrolling to the `searchFocusOffset` will be disabled.
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.
placeholderRenderer | any | PlaceholderRendererDefault | | Override the default placeholder component which is displayed when the tree is empty. This is an advanced option, and in most cases should probably be solved with custom CSS instead.

## Data Helper Functions

Expand Down
1 change: 0 additions & 1 deletion examples/storybooks/tree-to-tree.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { Component } from 'react';
import SortableTree from '../../src';


class App extends Component {
constructor(props) {
super(props);
Expand Down
24 changes: 24 additions & 0 deletions src/placeholder-renderer-default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import PropTypes from 'prop-types';
import styles from './placeholder-renderer-default.scss';

const PlaceholderRendererDefault = ({ isOver, canDrop }) =>
<div
className={
styles.placeholder +
(canDrop ? ` ${styles.placeholderLandingPad}` : '') +
(canDrop && !isOver ? ` ${styles.placeholderCancelPad}` : '')
}
/>;

PlaceholderRendererDefault.defaultProps = {
isOver: false,
canDrop: false,
};

PlaceholderRendererDefault.propTypes = {
isOver: PropTypes.bool,
canDrop: PropTypes.bool,
};

export default PlaceholderRendererDefault;
51 changes: 51 additions & 0 deletions src/placeholder-renderer-default.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
.placeholder {
position: relative;
height: 68px;
max-width: 300px;
padding: 10px;

&,
& > * {
box-sizing: border-box;
}

&::before {
border: 3px dashed #d9d9d9;
content: '';
position: absolute;
top: 5px;
right: 5px;
bottom: 5px;
left: 5px;
z-index: -1;
}
}

/**
* The outline of where the element will go if dropped, displayed while dragging
*/
.placeholderLandingPad {
border: none !important;
box-shadow: none !important;
outline: none !important;

* {
opacity: 0 !important;
}

&::before {
background-color: lightblue;
border-color: white;
}
}

/**
* Alternate appearance of the landing pad when the dragged location is invalid
*/
.placeholderCancelPad {
@extend .placeholderLandingPad;

&::before {
background-color: #e6a8ad;
}
}
24 changes: 23 additions & 1 deletion src/react-sortable-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import withScrolling, {
import 'react-virtualized/styles.css';
import TreeNode from './tree-node';
import NodeRendererDefault from './node-renderer-default';
import TreePlaceholder from './tree-placeholder';
import PlaceholderRendererDefault from './placeholder-renderer-default';
import {
walk,
getFlatDataFromTree,
Expand All @@ -34,6 +36,7 @@ import {
dndWrapRoot,
dndWrapSource,
dndWrapTarget,
dndWrapPlaceholder,
} from './utils/drag-and-drop-utils';
import styles from './react-sortable-tree.scss';

Expand All @@ -56,6 +59,10 @@ class ReactSortableTree extends Component {
treeIdCounter += 1;
this.dndType = dndType || this.treeId;
this.nodeContentRenderer = dndWrapSource(nodeContentRenderer, this.dndType);
this.treePlaceholderRenderer = dndWrapPlaceholder(
TreePlaceholder,
this.dndType
);
this.treeNodeRenderer = dndWrapTarget(TreeNode, this.dndType);

// Prepare scroll-on-drag options for this list
Expand Down Expand Up @@ -492,7 +499,15 @@ class ReactSortableTree extends Component {

let containerStyle = style;
let list;
if (isVirtualized) {
if (rows.length < 1) {
const Placeholder = this.treePlaceholderRenderer;
const PlaceholderContent = this.props.placeholderRenderer;
list = (
<Placeholder treeId={this.treeId} drop={this.drop}>
<PlaceholderContent />
</Placeholder>
);
} else if (isVirtualized) {
containerStyle = { height: '100%', ...containerStyle };

const ScrollZoneVirtualList = this.scrollZoneVirtualList;
Expand Down Expand Up @@ -625,6 +640,12 @@ ReactSortableTree.propTypes = {
// It is best to copy the component in `node-renderer-default.js` to use as a base, and customize as needed.
nodeContentRenderer: PropTypes.func,

// Override the default component for rendering an empty tree
// This is an advanced option for complete customization of the appearance.
// It is best to copy the component in `placeholder-renderer-default.js` to use as a base,
// and customize as needed.
placeholderRenderer: PropTypes.func,

// Determine the unique key used to identify each node and
// generate the `path` array passed in callbacks.
// By default, returns the index in the tree (omitting hidden nodes).
Expand Down Expand Up @@ -661,6 +682,7 @@ ReactSortableTree.defaultProps = {
isVirtualized: true,
maxDepth: null,
nodeContentRenderer: NodeRendererDefault,
placeholderRenderer: PlaceholderRendererDefault,
onMoveNode: null,
onVisibilityToggle: null,
reactVirtualizedListProps: {},
Expand Down
38 changes: 38 additions & 0 deletions src/tree-placeholder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { Children, cloneElement } from 'react';
import PropTypes from 'prop-types';

const TreePlaceholder = ({
children,
connectDropTarget,
treeId,
drop,
...otherProps
}) =>
connectDropTarget(
<div>
{Children.map(children, child =>
cloneElement(child, {
...otherProps,
})
)}
</div>
);

TreePlaceholder.defaultProps = {
canDrop: false,
draggedNode: null,
};

TreePlaceholder.propTypes = {
children: PropTypes.node.isRequired,

// Drop target
connectDropTarget: PropTypes.func.isRequired,
isOver: PropTypes.bool.isRequired,
canDrop: PropTypes.bool,
draggedNode: PropTypes.shape({}),
treeId: PropTypes.string.isRequired,
drop: PropTypes.func.isRequired,
};

export default TreePlaceholder;
32 changes: 32 additions & 0 deletions src/utils/drag-and-drop-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,24 @@ const nodeDropTarget = {
canDrop,
};

const placeholderDropTarget = {
drop(dropTargetProps, monitor) {
const { node, path, treeIndex } = monitor.getItem();
const result = {
node,
path,
treeIndex,
treeId: dropTargetProps.treeId,
minimumTreeIndex: 0,
depth: 0,
};

dropTargetProps.drop(result);

return result;
},
};

function nodeDragSourcePropInjection(connect, monitor) {
return {
connectDragSource: connect.dragSource(),
Expand All @@ -193,6 +211,16 @@ function nodeDropTargetPropInjection(connect, monitor) {
};
}

function placeholderPropInjection(connect, monitor) {
const dragged = monitor.getItem();
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
draggedNode: dragged ? dragged.node : null,
};
}

export function dndWrapSource(el, type) {
return dragSource(type, nodeDragSource, nodeDragSourcePropInjection)(el);
}
Expand All @@ -201,6 +229,10 @@ export function dndWrapTarget(el, type) {
return dropTarget(type, nodeDropTarget, nodeDropTargetPropInjection)(el);
}

export function dndWrapPlaceholder(el, type) {
return dropTarget(type, placeholderDropTarget, placeholderPropInjection)(el);
}

export function dndWrapRoot(el) {
return dragDropContext(HTML5Backend)(el);
}

0 comments on commit 2cd371c

Please sign in to comment.