Dragging and Dropping

The drag-and-drop functionality enables the user to move the TreeView items by dragging and dropping them within a single tree or across multiple trees.

Basics

To implement dragging and dropping in the TreeView:

  1. Set the draggable property to true.
  2. Handle the dispatched drag events.

To facilitate the event handling, the TreeView provides the following utilities:

Moving Items within a Single Tree

The following example demonstrates how to:

  • Implement the drag-and-drop functionality within a single TreeView by using all drag-and-drop utilities.
  • Update the expanded and selected item fields by using the processTreeViewItems function.
class App extends React.Component{
    dragClue;
    dragOverCnt = 0;
    isDragDrop = false;

    state = { tree, expand: { ids: [], idField: 'text' }, select: { ids: [], idField: 'text' } };

    render() {
        return (
            <div>
                <TreeView
                    draggable={true} onItemDragOver={this.onItemDragOver} onItemDragEnd={this.onItemDragEnd}
                    data={processTreeViewItems(
                        this.state.tree, { expand: this.state.expand, select: this.state.select }
                    )}
                    expandIcons={true} onExpandChange={this.onExpandChange} onItemClick={this.onItemClick}
                />
                <TreeViewDragClue ref={dragClue => this.dragClue = dragClue} />
            </div>
        );
    }

    onItemDragOver = (event) => {
        this.dragOverCnt++;
        this.dragClue.show(event.pageY + 10, event.pageX, event.item.text, this.getClueClassName(event));
    }
    onItemDragEnd = (event) => {
        this.isDragDrop = this.dragOverCnt > 0;
        this.dragOverCnt = 0;
        this.dragClue.hide();

        const eventAnalyzer = new TreeViewDragAnalyzer(event).init();

        if (eventAnalyzer.isDropAllowed) {
            const updatedTree = moveTreeViewItem(
                event.itemHierarchicalIndex,
                this.state.tree,
                eventAnalyzer.getDropOperation(),
                eventAnalyzer.destinationMeta.itemHierarchicalIndex,
            );

            this.setState({ tree: updatedTree });
        }
    }
    onItemClick = (event) => {
        if (!this.isDragDrop) {
            let ids = this.state.select.ids.slice();
            const index = ids.indexOf(event.item.text);

            index === -1 ? ids.push(event.item.text) : ids.splice(index, 1);

            this.setState({ select: { ids, idField: 'text' } });
        }
    }
    onExpandChange = (event) => {
        let ids = this.state.expand.ids.slice();
        const index = ids.indexOf(event.item.text);

        index === -1 ? ids.push(event.item.text) : ids.splice(index, 1);

        this.setState({ expand: { ids, idField: 'text' } });
    }

    getClueClassName(event) {
        const eventAnalyzer = new TreeViewDragAnalyzer(event).init();
        const { itemHierarchicalIndex: itemIndex } = eventAnalyzer.destinationMeta;

        if (eventAnalyzer.isDropAllowed) {
            switch (eventAnalyzer.getDropOperation()) {
                case 'child':
                    return 'k-i-plus';
                case 'before':
                    return itemIndex === '0' || itemIndex.endsWith(`${SEPARATOR}0`) ?
                        'k-i-insert-up' : 'k-i-insert-middle';
                case 'after':
                    const siblings = getSiblings(itemIndex, this.state.tree);
                    const lastIndex = Number(itemIndex.split(SEPARATOR).pop());

                    return lastIndex < siblings.length - 1 ? 'k-i-insert-middle' : 'k-i-insert-down';
                default:
                    break;
            }
        }

        return 'k-i-cancel';
    }
}

function getSiblings(itemIndex, data) {
    let result = data;

    const indices = itemIndex.split(SEPARATOR).map(index => Number(index));
    for (let i = 0; i < indices.length - 1; i++) {
        result = result[indices[i]].items;
    }

    return result;
}

const SEPARATOR = '_';
const tree = [{
    text: 'Furniture', expanded: true, items: [
        { text: 'Tables & Chairs' }, { text: 'Sofas' }, { text: 'Occasional Furniture' }]
}, {
    text: 'Decor', expanded: true, items: [
        { text: 'Bed Linen' }, { text: 'Curtains & Blinds' }, { text: 'Carpets' }]
}];

ReactDOM.render(
    <App />,
    document.querySelector('my-app')
);

Moving Items across Multiple Trees

The following example demonstrates how to:

  • Implement the drag-and-drop functionality across two TreeViews by using all drag-and-drop utilities.
  • Directly update the expanded and selected item fields.
class App extends React.Component{
    treeView1Guid;
    treeView2Guid;
    dragClue;
    dragOverCnt = 0;
    isDragDrop = false;

    state = { tree, tree2 };

    render() {
        return (
            <div>
                <TreeView data={this.state.tree}
                    draggable={true} onItemDragOver={this.onItemDragOver} onItemDragEnd={this.onItemDragEnd}
                    ref={treeView => this.treeView1Guid = treeView && treeView.guid}
                    expandIcons={true} onExpandChange={this.onExpandChange} onItemClick={this.onItemClick}
                />
                <TreeView data={this.state.tree2}
                    draggable={true} onItemDragOver={this.onItemDragOver} onItemDragEnd={this.onItemDragEnd}
                    ref={treeView => this.treeView2Guid = treeView && treeView.guid}
                    expandIcons={true} onExpandChange={this.onExpandChange} onItemClick={this.onItemClick}
                />
                <TreeViewDragClue ref={dragClue => this.dragClue = dragClue} />
            </div>
        );
    }

    onItemDragOver = (event) => {
        this.dragOverCnt++;
        this.dragClue.show(event.pageY + 10, event.pageX, event.item.text, this.getClueClassName(event));
    }
    onItemDragEnd = (event) => {
        this.isDragDrop = this.dragOverCnt > 0;
        this.dragOverCnt = 0;
        this.dragClue.hide();

        const eventAnalyzer = new TreeViewDragAnalyzer(event).init();

        if (eventAnalyzer.isDropAllowed) {
            const { sourceData, targetData } = moveTreeViewItem(
                event.itemHierarchicalIndex,
                this.resolveData(event.target.guid),
                eventAnalyzer.getDropOperation(),
                eventAnalyzer.destinationMeta.itemHierarchicalIndex,
                this.resolveData(eventAnalyzer.destinationMeta.treeViewGuid)
            );

            this.setState({
                [this.resolveDataKey(event.target.guid)]: sourceData,
                [this.resolveDataKey(eventAnalyzer.destinationMeta.treeViewGuid)]: targetData
            });
        }
    }
    onItemClick = (event) => {
        if (!this.isDragDrop) {
            event.item.selected = !event.item.selected;
            this.forceUpdate();
        }
    }
    onExpandChange = (event) => {
        event.item.expanded = !event.item.expanded;
        this.forceUpdate();
    }

    getClueClassName(event) {
        const eventAnalyzer = new TreeViewDragAnalyzer(event).init();
        const { itemHierarchicalIndex: itemIndex, treeViewGuid } = eventAnalyzer.destinationMeta;

        if (eventAnalyzer.isDropAllowed) {
            switch (eventAnalyzer.getDropOperation()) {
                case 'child':
                    return 'k-i-plus';
                case 'before':
                    return itemIndex === '0' || itemIndex.endsWith(`${SEPARATOR}0`) ?
                        'k-i-insert-up' : 'k-i-insert-middle';
                case 'after':
                    const siblings = getSiblings(itemIndex, this.resolveData(treeViewGuid));
                    const lastIndex = Number(itemIndex.split(SEPARATOR).pop());

                    return lastIndex < siblings.length - 1 ? 'k-i-insert-middle' : 'k-i-insert-down';
                default:
                    break;
            }
        }

        return 'k-i-cancel';
    }
    resolveData(treeViewGuid) {
        return treeViewGuid === this.treeView1Guid ? this.state.tree : this.state.tree2;
    }
    resolveDataKey(treeViewGuid) {
        return treeViewGuid === this.treeView1Guid ? 'tree' : 'tree2';
    }
}

function getSiblings(itemIndex, data) {
    let result = data;

    const indices = itemIndex.split(SEPARATOR).map(index => Number(index));
    for (let i = 0; i < indices.length - 1; i++) {
        result = result[indices[i]].items;
    }

    return result;
}

const SEPARATOR = '_';
const tree = [{
    text: 'Furniture', expanded: true, items: [
        { text: 'Tables & Chairs' }, { text: 'Sofas' }, { text: 'Occasional Furniture' }]
}, {
    text: 'Decor', expanded: true, items: [
        { text: 'Bed Linen' }, { text: 'Curtains & Blinds' }, { text: 'Carpets' }]
}];

const tree2 = [{
    text: 'Storage', expanded: true, items: [
        { text: 'Wall Shelving' }, { text: 'Floor Shelving' }, { text: 'Kids Storage' }]
}, {
    text: 'Lights', expanded: true, items: [
        { text: 'Ceiling' }, { text: 'Table' }, { text: 'Floor' }]
}];

ReactDOM.render(
    <App />,
    document.querySelector('my-app')
);
 /