Ace List, Tree, Datagrid

When creating a complex user interfaces it's often required to add elements that allow to the user to select one or more items. The AceTree widget can act as a list, a tree, a datagrid and a datagrid-tree. It is available as the List, Tree and Datagrid plugin in Cloud9.

📘

Based on Ace

The AceTree is based on Ace, which is the text editor that Cloud9 uses and is developed by the Cloud9 core team. The Ace editor uses a method called 'virtual viewport' to render it's content. This makes it possible to load a document with 4 million lines in ace. We have used the same technique for AceTree. You can therefore load enormous amounts of data into these widgets without any impact on performance. This makes this ui element especially suited for building professional, scalable UIs.

Topics

🚧

Example Code

In the next examples we'll load the UI elements inside a dialog. For more information on creating a dialog see the dialog guide.

As a List

The most basic form the AceTree widget can take is that of a list. Let's start of with a simple example and then add more advanced features to show you what the API looks like.

A very basic list rendering two items.

var simpleList = new List({}, plugin);
simpleList.attachTo(htmlParent);
simpleList.setRoot(["Item 1", "Item 2"]);
15641564

The object passed as first argument of the constructor allows you to configure the style and behavior of the list. In the next example we'll set the container of the list explicitly so that we don't have to call attachTo(), in addition we configure the list to read data from objects instead of strings.

👍

Every config property can also be set as a property on the widget

You can set these properties both when creating the widget in it's constructor or by setting them directly on the widget after creation.

var myList = new List({
    container: htmlParent,
    emptyMessage: "No items found",
    dataType: "object"
}, plugin);

myList.setRoot([
    { label: "Item 1" }, 
    { label: "Item 2" } 
]);

This list looks exactly the same as the one in the previous example. A big difference is that you now control the objects that are manipulated by the list. The myList.selectedNode will return your object.

❗️

Objects are extended!

Be aware that AceTree adds a small set of properties to your objects. If you want to prevent this, make sure you create copies of your objects before calling setRoot() with them.

Custom Renderer

One of the key features of AceTree, besides it's scalability, is your ability to determine how it renders your data. The following example loads a set of custom objects and renders them using a custom row renderer which generates the html that is used to display each item.

customList = new List({
    container: htmlParent,
    rowHeight : 38,
    dataType: "object",

    renderRow: function(row, html, config){ 
        var node = this.root.items[row];
        var isSelected = this.isSelected(node);
    
        html.push("<div class='" + (isSelected ? "selected " : "") + "'"
            + " style='height:28px;padding:5px;'>" 
            + "<strong>" + node.head + "</strong><br />"
            + node.body + "</div>");
    }
}, plugin);

customList.setRoot([
    { head: "Hey You All!", body: "This is a longer way to say hi to all of you folks!" }, 
    { head: "Buy More!", body: "Buying more has been long known to improve health and longevity" } 
]);
15701570

As an alternative to renderRow() there is a set of functions that allow you to set specific html for the caption, icon and other elements. We'll show case those functions when customizing the tree.

As a Tree

The next form the AceTree widget can take is that of a tree. Let's start of with a simple example and then add more advanced features to show you what the API looks like.

A very basic tree rendering two items.

var simpleTree = new Tree({}, plugin);
simpleTree.attachTo(htmlParent);

simpleTree.setRoot({
    label: "root",
    items: [{
        label: "test",
        items: [
            { label: "sub1" },
            { label: "sub2" }
        ]
    }, { label: "test2" } ]});
});
15701570

You might have noticed that we didn't use dataType. This is because the Tree and Datagrid always expect objects as data.

Custom Renderer

You can customize the tree in the same way as we've done with the list in the examples above. This time, instead of using the renderRow() function, we'll define custom renders for only a part of the item, in this case the icon. We have defined three classes in our css called .folder, .default and .loading. We'll set these classes in our getIconHTML() function to set the icon of each row.

var customTree = new Tree({
    container: htmlParent,
    
    getIconHTML: function(node) {
        var icon = node.isFolder ? "folder" : "default";
        if (node.status === "loading") icon = "loading";
        return "<span class='ace_tree-icon " + icon + "'></span>";
    }
}, plugin);

customTree.setRoot({
    label: "root",
    items: [{
        label: "test",
        isFolder: true,
        items: [
            { label: "sub1" },
            { label: "sub2" }
        ]
    }, { label: "test2", isFolder: true } ]});
15701570

As a Datagrid

The next form the AceTree widget can take is that of a tree. The columns array specifies each of the columns. The caption determines the text displayed on the column header. The value is used as the property read from each node. You can mix widths specified in percentage and in pixels, just make sure the percentage totals to 100%.

A basic datagrid rendering two items.

var simpleDatagrid = new Datagrid({
    container: htmlParent,

    columns : [
        {
            caption: "Property",
            value: "label",
            width: "40%",
        }, 
        {
            caption: "Value",
            value: "value",
            width: "60%",
        }, 
        {
            caption: "Type",
            value: "type",
            width: "55"
        },
        {
            caption: "CPU",
            getText: function(node) { return node.cpu + "%"; },
            width: "50",
        }
    ]
}, plugin);

myDatagrid.setRoot([
    { label: "test", value: "Example Value", type: "50", cpu: "20" }, 
    { label: "test2", value: "Example Value 2", type: "50", cpu: "50" } 
]);

The getText() function can be used instead of the value attribute. The value that is returned by the getText() function is used as the contents of a cell.

15701570

👍

rowHeight is calculated automatically!

Note that for the datagrid the rowHeight is measured based on the css. You can override this behavior by setting rowHeight explicitly.

As a Datagrid-Tree

The last form the AceTree widget can take is that of a combined datagrid and tree.

A basic datagrid-tree with a few sub nodes and an icon handler.

var simpleDatagridTree = new Datagrid({
    container: htmlParent,

    columns : [
        {
            caption: "Property",
            value: "name",
            defaultValue: "Scope",
            width: "40%",
            type: "tree"
        }, 
        {
            caption: "Value",
            value: "value",
            width: "60%"
        }, 
        {
            caption: "Type",
            value: "type",
            width: "55"
        },
        {
            caption: "CPU",
            getText: function(node) { return (node.cpu || 0) + "%"; },
            width: "50",
        }
    ],
    
    getIconHTML: function(node) {
        return "<span class='dbgVarIcon'></span>";
    }
}, plugin);

simpleDatagridTree.setRoot({
    label: "root",
    items: [{
        label: "test",
        items: [
            {label: "sub1", value: "Example Value", type: "50", cpu: "20" },
            {label: "sub2", value: "Example Value", type: "50", cpu: "20" }
        ]
    }, {label: "test2", value: "Example Value", type: "50", cpu: "20" } ]});
});
15701570

Events & Interaction

The AceTree widgets fire 20 events. In this section we'll discuss 6 commonly used once. We'll mention many of the other events in the sections below.

Click event

The click is fired whenever the user clicks on the widget. You get information about the x and y position of the click as well as access to the native browser event.

simpleTree.on("click", function(e){
    console.log("The user clicked at ", e.x, ", ", e.y);
});

Delete event

The delete event is called when the user deletes an item from the widget, for instance by hitting the DEL button. Note that the widget won't actually manipulate your data object. That is up to you. If you don't remove the selected nodes from the data, they will still be there.

simpleList.on("delete", function(){
    // Manipulate data object here
});

Scroll event

The scroll event is called when the user vertically scrolls through the widget. Remember that these widgets have a virtual viewport and thus only render the content that is on the screen.

myDatagrid.on("scroll", function(){
    console.log("The scroll position is", myDatagrid.scrollTop);
});

To set the scroll simply set the scrollTop property.

myDatagrid.scrollTop = 100;

Resize event

The resize event is called when the resize() method is called and the size is actually changed.

myDatagrid.on("resize", function(){
    console.log("This was an actual resize");
});

Expand & collapse events

There are two events that are specific to the tree (and datagrid-tree). These are expand and collapse events. They'll fire when the user expands or collapses an item.

simpleTree.on("expand", function(node){
    console.log("You expanded", node.label);
});

Selection

The selection allows you to manipulate the selection through the select() method. Selection is done by reference and you can select one node or an array of multiple nodes.

myList.select(data[5]); // Select the 5th item
myList.select(data); // Selects all items

At any time retrieve the current selection via one of two properties.

myList.selectedNodes.forEach(function(node){
    console.log(node.label, "is selected);
});

console.log(myList.selectedNode.label, "is the caret");

Listen to the user selecting items using the select events.

myList.on("select", function(){
    console.log("The user selected a node", myList.selectedNode.label);
});

You can disable the ability to select a node by setting the property noSelect to true.

Reloading the data

Cloud9 plugins often want to update the data loaded in the widget. As you update your dataset you need to call the refresh() method to update the widgets view.

simpleTree.root.items[0].label = "New Name";
simpleTree.refresh();

Renaming and Editing

The AceTree widgets have a built-in feature that allows renaming nodes in the List and Tree and to edit a cell in the Datagrid.

For both the Tree and the List simple enable rename to enable this behavior.

myTree.enableRename = true;

Now a user can hit F2 or click once on an already selected node to start renaming the item. You can also programmatically trigger renaming.

myList.startRename(myList.selectedNode);
14701470

The Datagrid allows you to configure a cell as editable. Simply set the editor property to "textbox" of the appropriate column. You must set enableRename to true to make editing work.

{
    caption: "Value",
    value: "value",
    width: "60%",
    editor: "textbox"
},

Now a user can double click on a cell to start renaming the item. You can also programmatically trigger renaming.

datagrid.startRename(myList.selectedNode, 1); // The 2nd argument is the column index
15701570

Rename Events

There are two events related to renaming; beforeRename and afterRename. Use the beforeRename event to validate whether your plugin allows renaming the selected node. You can call e.preventDefault() to disable renaming before it starts. The afterRename event is called when the rename is complete. Just like with the delete event you are responsible for updating the dataset and refreshing the widget.

datagrid.on("beforeRename", function(e) {
    if (e.column.caption == "Value" && e.node.isFolder)
        return e.preventDefault();
});
datagrid.on("afterRename", function(e) {
    sendToServer(e.node.id, e.value, function(err){
        e.node.label = e.value;
        datagrid.refresh();
    });
});

Expanding

To expand a node when setting the data, so before it is loaded in the tree, set isOpen to true.

simpleDatagridTree.setRoot({
    items: [{
        label: "test",
        isOpen: true,
        items: [
            {label: "sub1", value: "Example Value", type: "50", cpu: "20" },
            {label: "sub2", value: "Example Value", type: "50", cpu: "20" }
        ]
    }]
});

Sorting

To influence the sorting of the widget simply add a sort function. This function gets all children of a node (or all nodes for the list) and allows you to sort the list in any way you like.

This example sorts a list alphabetical and makes sure that any folders are at the top of the list.

sort: function(children) {
    var compare = simpleTree.model.alphanumCompare;
    return children.sort(function(a, b) {
        var aIsSpecial = a.tagName == "folder";
        var bIsSpecial = b.tagName == "folder";
        if (aIsSpecial && !bIsSpecial) return 1;
        if (!aIsSpecial && bIsSpecial) return -1;
        if (aIsSpecial && bIsSpecial) return a.index - b.index;
        
        return compare(a.name + "", b.name + "");
    });
};

Filtering

One of the common interactions, that you'll find throughout the Cloud9 UI is to let the user filter the data in a widget. We've added a very simple way to filter based on a single keyword.

This example combines a textbox with a tree and allows the user to search for items in the tree by typing in the textbox.

htmlParent.innerHTML = "<input class='my-style' />"
htmlParent.firstChild.addEventListener("keyup", function(){
    simpleTree.filterKeyword = this.value;
});

var simpleTree = new Tree({
    container: htmlParent
}, plugin);

simpleTree.setRoot({
    label: "root",
    items: [{
        label: "test",
        items: [
            {label: "sub1"},
            {label: "sub2"}
        ]
    }, {label: "test2"} ]});
15701570

Now a user can type into the textbox to only show those elements matching the characters typed.

15701570

There are two properties that you can set to configure the filter. The filterCaseInsensitive property when set to false will filter based on case sensitive characters. The filterProperty can be set to a string containing the name of the property on your objects to use for filtering. If this property is not set either label or name are used.

Checkboxes

To enable checkboxes in your list, tree or datagrid set enableCheckboxes to true. Each row will display a checkbox which can be set to checked, unchecked and half checked.

datagrid = new Datagrid({
    container: htmlParent,
    enableCheckboxes: true,
    
    columns : [
        {
            caption: "Name",
            value: "label",
            width: "35%",
            type: "tree"
        }, 
        {
            caption: "Description",
            value: "description",
            width: "65%"
        }
    ]
}, plugin);

simpleDatagridTree.setRoot({
    items: [{
        label: "test",
        isChecked: true
        items: [
            { label: "sub1", description: "Example Value" },
            { label: "sub2", description: "Example Value" }
        ]
    });
});

Setting isChecked to true on a item will display it as checked. Set it to -1 to have it half-checked and false for unchecked. By default the widget will only toggle between fully checked and unchecked. Setting items as half-checked will require custom code.

This is what that looks like in the Cloud9 installer.

15701570

Checkbox methods

To check an item call the check() method.

datagrid.check(item);

Similarly to uncheck an item call the uncheck() method.

datagrid.uncheck(item);

Checkbox events

When one or more items are checked and unchecked their respective events are called.

datagrid.on("check", function(items){
    console.log("checked", items.map(function(n){ return n.label; }).join(" "));
});
datagrid.on("uncheck", function(items){
    console.log("unchecked", items.map(function(n){ return n.label; }).join(" "));
});

Resizing

It is important to know that the AceTree widgets don't resize automatically. Make sure to call the resize() method whenever they should recalculate their size. By default the resize() method will only trigger if the dimensions of the container have changed, so you can call it as many times as you want. It will also only recompute the area that actually needs it.

This resizes the tree when the browser resizes.

layout.on("resize", function(){
    simpleTree.resize();
}, plugin);

🚧

Check the resize event of your container

Most Cloud9 UI plugins such as Dialogs and Panels offer a resize event. Simply hook that event and call widget.resize() to make sure your widget is always correctly rendered.

Theming

Theming of these widgets is done through css. By default the .custom-tree class is applied for List and Tree and the .blackdg class for Datagrid. You can choose to override this and use your own class by setting the theme property. Note that the default themes are configured using the Cloud9 main theme. More information on theming will be documented soon.

widget.theme = "my-own-class"; // Or set theme in the constructor options

Drag & Drop

The AceTree widgets support drag&drop out of the box. Simply enable it by setting the enableDragDrop property to true.

widget.enableDragDrop = true;

Drag & Drop Styling

You control the look of the drag highlighting via css. Simply reference the classes that are used and overwrite any styling you want.

dragOveris set on the container when dragging
dragImageclass of the node which is dragged
dropTargetclass of the parent item that the node is hovering over
dragHighlightclass of the children container of the parent that the node is hovering over
15701570

Drag & Drop Events

The following events allow you to control the drag&drop operations. Similar to the delete and rename events it is your responsibility to update the underlying data.

simpleTree.on("drop", function(e) {
    if (e.target && e.selectedNodes) {
        if (e.isCopy)
            copy(e.selectedNodes, e.target);
        else
            move(e.selectedNodes, e.target);
    }
});

The isCopy property is set when the user hold the Ctrl or Command key.

dropFired when the node or nodes are dropped
dragInFires when a node is dragging into the widget
dragOutFires when a node is dragging out of the widget
dragMoveOutsideFires when the node is moves outside of the widget
folderDragEnterFires when a node drags onto a parent
folderDragLeaveFires when a node drags out of a parent
dropOutsideFires when a node is dropped outside of the widget

Commands

Similar to Ace and Cloud9 itself, the AceTree widgets have a set of commands that you can execute on the widget.

simpleTree.execCommand("selectAll");

The following table lists all the commands available.

selectAllSelects all the elements in the widget
centerselectionScrolls the selection into the center of the widget
closeOrlevelUpCollapses the selected node or moves the caret one level up
levelUpMoves the caret one level up
levelDownMoves the caret one level down
goToStartMoves the caret to the start
goToEndMoves the caret to the end
closeAllFromSelectedCollapses all selected nodes
openAllFromSelectedExpand all selected nodes
pageupScroll one page up
gotopageupMove the caret one page up
pagedownScroll one page down
gotopageDownMove the caret one page down
scrollupScroll up
scrolldownScroll down
goUpMove the caret one position up
goDownMove the caret one position down
selectUpSelect up
selectDownSelect down
selectToUpunselect current node and select node above, keeping other selected nodes
selectToDownunselect current node and select node below, keeping other selected nodes
selectMoreUpselect node above cursor, keeping all other nodes selected
selectMoreDownselect node below cursor, keeping all other nodes selected
renameStart rename
choseSelect and call the choose event
deleteCall the delete event

Misc

One last edge case we found useful when building the Debugger UI was to disable the scrolling behavior of the datagrid and instead have it render all the content. To enable this use the minLines and maxLines properties to set the minimum and maximum amount of lines that should be rendered by the widget before it starts showing a scrollbar.

var myDatagrid = new Datagrid({
    container: htmlParent,
    minLines: 3,
    maxLines: 200
}, plugin);

myDatagrid.setRoot(dataset);