@atlaskit/tree
Version:
A React Component for displaying expandable and sortable tree hierarchies
191 lines • 8.33 kB
JavaScript
import React, { Component } from 'react';
import { Draggable, Droppable, DragDropContext, } from 'react-beautiful-dnd-next';
import { getBox } from 'css-box-model';
import { calculateFinalDropPositions } from './Tree-utils';
import { noop } from '../../utils/handy';
import { flattenTree, mutateTree } from '../../utils/tree';
import TreeItem from '../TreeItem';
import { getDestinationPath, getItemById, getIndexById, } from '../../utils/flat-tree';
import DelayedFunction from '../../utils/delayed-function';
export default class Tree extends Component {
constructor() {
super(...arguments);
this.state = {
flattenedTree: [],
draggedItemId: undefined,
};
// HTMLElement for each rendered item
this.itemsElement = {};
this.expandTimer = new DelayedFunction(500);
this.onDragStart = (result) => {
const { onDragStart } = this.props;
this.dragState = {
source: result.source,
destination: result.source,
mode: result.mode,
};
this.setState({
draggedItemId: result.draggableId,
});
if (onDragStart) {
onDragStart(result.draggableId);
}
};
this.onDragUpdate = (update) => {
const { onExpand } = this.props;
const { flattenedTree } = this.state;
if (!this.dragState) {
return;
}
this.expandTimer.stop();
if (update.combine) {
const { draggableId } = update.combine;
const item = getItemById(flattenedTree, draggableId);
if (item && this.isExpandable(item)) {
this.expandTimer.start(() => onExpand(draggableId, item.path));
}
}
this.dragState = {
...this.dragState,
destination: update.destination,
combine: update.combine,
};
};
this.onDropAnimating = () => {
this.expandTimer.stop();
};
this.onDragEnd = (result) => {
const { onDragEnd, tree } = this.props;
const { flattenedTree } = this.state;
this.expandTimer.stop();
const finalDragState = {
...this.dragState,
source: result.source,
destination: result.destination,
combine: result.combine,
};
this.setState({
draggedItemId: undefined,
});
const { sourcePosition, destinationPosition } = calculateFinalDropPositions(tree, flattenedTree, finalDragState);
onDragEnd(sourcePosition, destinationPosition);
this.dragState = undefined;
};
this.onPointerMove = () => {
if (this.dragState) {
this.dragState = {
...this.dragState,
horizontalLevel: this.getDroppedLevel(),
};
}
};
this.calculateEffectivePath = (flatItem, snapshot) => {
const { flattenedTree, draggedItemId } = this.state;
if (this.dragState &&
draggedItemId === flatItem.item.id &&
(this.dragState.destination || this.dragState.combine)) {
const { source, destination, combine, horizontalLevel, mode, } = this.dragState;
// We only update the path when it's dragged by keyboard or drop is animated
if (mode === 'SNAP' || snapshot.isDropAnimating) {
if (destination) {
// Between two items
return getDestinationPath(flattenedTree, source.index, destination.index, horizontalLevel);
}
if (combine) {
// Hover on other item while dragging
return getDestinationPath(flattenedTree, source.index, getIndexById(flattenedTree, combine.draggableId), horizontalLevel);
}
}
}
return flatItem.path;
};
this.isExpandable = (item) => !!item.item.hasChildren && !item.item.isExpanded;
this.getDroppedLevel = () => {
const { offsetPerLevel } = this.props;
const { draggedItemId } = this.state;
if (!this.dragState || !this.containerElement) {
return undefined;
}
const containerLeft = getBox(this.containerElement).contentBox.left;
const itemElement = this.itemsElement[draggedItemId];
if (itemElement) {
const currentLeft = getBox(itemElement).contentBox.left;
const relativeLeft = Math.max(currentLeft - containerLeft, 0);
return (Math.floor((relativeLeft + offsetPerLevel / 2) / offsetPerLevel) + 1);
}
return undefined;
};
this.patchDroppableProvided = (provided) => {
return {
...provided,
innerRef: (el) => {
this.containerElement = el;
provided.innerRef(el);
},
};
};
this.setItemRef = (itemId, el) => {
if (!!el) {
this.itemsElement[itemId] = el;
}
};
this.renderItems = () => {
const { flattenedTree } = this.state;
return flattenedTree.map(this.renderItem);
};
this.renderItem = (flatItem, index) => {
const { isDragEnabled } = this.props;
return (React.createElement(Draggable, { key: flatItem.item.id, draggableId: flatItem.item.id.toString(), index: index, isDragDisabled: !isDragEnabled }, this.renderDraggableItem(flatItem)));
};
this.renderDraggableItem = (flatItem) => (provided, snapshot) => {
const { renderItem, onExpand, onCollapse, offsetPerLevel } = this.props;
const currentPath = this.calculateEffectivePath(flatItem, snapshot);
if (snapshot.isDropAnimating) {
this.onDropAnimating();
}
return (React.createElement(TreeItem, { key: flatItem.item.id, item: flatItem.item, path: currentPath, onExpand: onExpand, onCollapse: onCollapse, renderItem: renderItem, provided: provided, snapshot: snapshot, itemRef: this.setItemRef, offsetPerLevel: offsetPerLevel }));
};
}
static getDerivedStateFromProps(props, state) {
const { draggedItemId } = state;
const { tree } = props;
const finalTree = Tree.closeParentIfNeeded(tree, draggedItemId);
const flattenedTree = flattenTree(finalTree);
return {
...state,
flattenedTree,
};
}
static closeParentIfNeeded(tree, draggedItemId) {
if (!!draggedItemId) {
// Closing parent internally during dragging, because visually we can only move one item not a subtree
return mutateTree(tree, draggedItemId, {
isExpanded: false,
});
}
return tree;
}
render() {
const { isNestingEnabled } = this.props;
const renderedItems = this.renderItems();
return (React.createElement(DragDropContext, { onDragStart: this.onDragStart, onDragEnd: this.onDragEnd, onDragUpdate: this.onDragUpdate },
React.createElement(Droppable, { droppableId: "tree", isCombineEnabled: isNestingEnabled, ignoreContainerClipping: true }, (provided) => {
const finalProvided = this.patchDroppableProvided(provided);
return (React.createElement("div", Object.assign({ ref: finalProvided.innerRef, style: { pointerEvents: 'auto' }, onTouchMove: this.onPointerMove, onMouseMove: this.onPointerMove }, finalProvided.droppableProps),
renderedItems,
provided.placeholder));
})));
}
}
Tree.defaultProps = {
tree: { children: [] },
onExpand: noop,
onCollapse: noop,
onDragStart: noop,
onDragEnd: noop,
renderItem: noop,
offsetPerLevel: 35,
isDragEnabled: false,
isNestingEnabled: false,
};
//# sourceMappingURL=Tree.js.map