1 Dec 2022

Custom Tree Views

Sometimes, when developing viewer-based applications, you may want to display some sort of a custom, hierarchical information in form of a tree view. While there are 3rd party libraries for this exact purpose (for example, https://www.jstree.com or http://inspire-tree.com), if all you need is a simple, static tree view, there's actually one you can use directly in the Viewer's UI system. Let's take a look at it!

Under the Autodesk.Viewing.UI namespace there are two classes - Tree and TreeDelegate - that are used to visualize different kinds of hierarchical data, for example, design properties or layers. These two classes are closely tied together. The Tree class is the visual component - it is responsible for building the HTML content representing the tree inside a specified container, and handling user events. The TreeDelegate is an abstract class responsible for answering questions about a specific data structure such as "what is the display name for this node?", "does this node have any children?", or "what should happen if the user clicks on this node?". If you have a custom data structure that you would like display using the viewer's native UI, you can create your own instance of the Tree class, and provide your own implementation of the TreeDelegate interface.

The TreeDelegate abstract class contains various methods, some of them have a default implementation, and some of them need to be implemented. Let's take a look at the methods that are actually used by the Tree class:

For clarity, the method signatures below include TypeScript type annotations

  • isTreeNodeGroup(node: any): boolean (required) - must return true if the node is a group node (in the tree, group nodes have a small arrow icon that can be used to expand/collapse the nodes children)
  • getTreeNodeId(node: any): string (required) - must return a unique ID for specific node (used in a custom attribute of the corresponding HTML element)
  • getTreeNodeLabel(node: any): string (required) - must return display name for specific node
  • getTreeNodeClass(node: any): string (optional) - returns CSS class name for specific node (returns "" by default)
  • onTreeNodeClick(tree: Tree, node: any, event: Event) (optional) - will be called when the user clicks on a specific tree node
  • onTreeNodeDoubleClick(tree: Tree, node: any, event: Event) (optional) - will be called when the user double-clicks on a specific tree node
  • onTreeNodeRightClick(tree: Tree, node: any, event: Event) (optional) - will be called when the user right-clicks on a specific tree node
  • onTreeNodeHover(tree: Tree, node: any, event: Event) (optional) - will be called when the user hovers a specific tree node
  • forEachChild(node: any, callback) (optional) - should iterate over all children of a specific node, and execute the callback function for each child (by default, the method looks for a children property on the node and iterates over its elements)
  • shouldCreateTreeNode(node: any): boolean (optional) - returns boolean value indicating whether this specific node should be displayed in the tree (true by default)
  • createTreeNode(node: any, parent: any, options: any, type: 'group' | 'leaf', depth: number): HTMLLabelElement (optional) - generates corresponding HTML markup for specific node; can be overriden to customize the generated HTML

As an example, consider the following (fictional) hierarchical data representing a folder structure:

const MY_FOLDER_DATA = {
    path: '/home',
    name: 'Home',
    entries: [
        {
            path: '/home/docs',
            name: 'Documents',
            entries: [
                {
                    path: '/home/docs/drawing1.pdf',
                    name: 'drawing1.pdf'
                }
            ]
        },
        {
            path: '/home/images',
            name: 'Images',
            entries: [
                {
                    path: '/home/images/image1.png',
                    name: 'image1.png'
                },
                {
                    path: '/home/images/old',
                    name: 'Old',
                    entries: [
                        {
                            path: '/home/images/old/image1.png',
                            name: 'image1.png'
                        },
                        {
                            path: '/home/images/old/image2.png',
                            name: 'image2.png'
                        }                        
                    ]
                }
            ]
        }
    ]
};

If we wanted to display this kind of structure in the tree, we could implement a custom tree delegate class like so:

class CustomTreeDelegate extends Autodesk.Viewing.UI.TreeDelegate {
    isTreeNodeGroup(node) {
        return node.entries && node.entries.length > 0;
    }

    getTreeNodeId(node) {
        return node.path;
    }

    getTreeNodeLabel(node) {
        return node.name;
    }

    getTreeNodeClass(node) {
        node.children && node.children.length > 0 ? 'group' : 'leaf';
    }

    forEachChild(node, callback) {
        for (const child of node?.entries) {
            callback(child);
        }
    }
}

And finally, we would pass this delegate to the instance of our tree:

// ...

const container = document.getElementById('some-container');
const delegate = new CustomTreeDelegate();
const tree = new Autodesk.Viewing.UI.Tree(delegate, MY_FOLDER_DATA, container);

// ...

And that's it!

Here's a more complete sample that wraps our custom tree in a Autodesk.Viewing.UI.DockingPanel, and updates some of the CSS styling of the generated HTML as well:

const MY_FOLDER_DATA = {
    path: '/home',
    name: 'Home',
    entries: [
        {
            path: '/home/docs',
            name: 'Documents',
            entries: [
                {
                    path: '/home/docs/drawing1.pdf',
                    name: 'drawing1.pdf'
                }
            ]
        },
        {
            path: '/home/images',
            name: 'Images',
            entries: [
                {
                    path: '/home/images/image1.png',
                    name: 'image1.png'
                },
                {
                    path: '/home/images/old',
                    name: 'Old',
                    entries: [
                        {
                            path: '/home/images/old/image1.png',
                            name: 'image1.png'
                        },
                        {
                            path: '/home/images/old/image2.png',
                            name: 'image2.png'
                        }                        
                    ]
                }
            ]
        }
    ]
};

class CustomTreeDelegate extends Autodesk.Viewing.UI.TreeDelegate {
    isTreeNodeGroup(node) {
        return node.entries && node.entries.length > 0;
    }

    getTreeNodeId(node) {
        return node.path;
    }

    getTreeNodeLabel(node) {
        return node.name;
    }

    getTreeNodeClass(node) {
        node.children && node.children.length > 0 ? 'group' : 'leaf';
    }

    forEachChild(node, callback) {
        for (const child of node?.entries) {
            callback(child);
        }
    }

    onTreeNodeClick(tree, node, event) {
        console.log('click', tree, node, event);
    }

    onTreeNodeDoubleClick(tree, node, event) {
        console.log('double-click', tree, node, event);
    }

    onTreeNodeRightClick(tree, node, event) {
        console.log('right-click', tree, node, event);
    }

    createTreeNode(node, parent, options, type, depth) {
        const label = super.createTreeNode(node, parent, options, type, depth);
        const icon = label.previousSibling;
        const row = label.parentNode;
        // Center arrow icon
        if (icon) {
            icon.style.backgroundPositionX = '5px';
            icon.style.backgroundPositionY = '5px';
        }
        // Offset rows depending on their tree depth
        row.style.padding = `5px`;
        row.style.paddingLeft = `${5 + (type === 'leaf' ? 20 : 0) + depth * 20}px`;
        return label;
    }
}

export class CustomTreeViewPanel extends Autodesk.Viewing.UI.DockingPanel {
    constructor(viewer, id, title) {
        super(viewer.container, id, title);
        this.container.classList.add('property-panel'); // Re-use some handy defaults
        this.container.dockRight = true;
        this.createScrollContainer({ left: false, heightAdjustment: 70, marginTop: 0 });
        this.delegate = new CustomTreeDelegate();
        this.tree = new Autodesk.Viewing.UI.Tree(this.delegate, MY_FOLDER_DATA, this.scrollContainer);
    }
}

You can also find this sample code ready-to-run in the custom-tree-viewer branch of our GitHub repo https://github.com/autodesk-platform-services/aps-simple-viewer-nodejs.

Related Article