Trees and XULCustomTree
Trees are a traditional subject of hurt in webapplications, since they can get very complex and heavy, and a painful influence on performance. Not in XUL though, Mozilla implements a
tree element that can be used two ways. One way is by using elements for every part of the tree content, the other is by defining a custom tree view that implements a complex interface through which the tree gets handed every small aspect of what, where and when it should draw. An example of this implementation are the folder and message views in Thunderbird. In SiteFusion, a full implementation of this custom tree view is present, allowing you to use all features that Mozilla enables, like drag and drop, editable cells, sorting, images, checkboxes and progress meters. This implementation is suitable for very large trees because it uses smaller, more specialized objects and classes to communicate and display the contents of the tree and because it supports sparse trees, which means that only the visible parts of the tree are sent to the client. However, for simpler trees, the element implementation is easier. Both will be described here. For an extensive example of the use of XULCustomTree, see
CustomTree Demo.
A Simple Tree
For simple trees, the basic buildup consists of a
XULTree object containing a
XULTreeCols object and a
XULTreeChildren object. The XULTreeCols object contains a
XULTreeCol object for every column, and the XULTreeChildren object will contain the items in the tree. These items have a specific configuration as well. An item consists of a
XULTreeItem object containing a
XULTreeRow and, if it is a container item for other items, a XULTreeChildren object. The XULTreeRow object contains a
XULTreeCell object for every cell in the row. However in very simple trees that have only one column, you can suffice with only the XULTreeItem object with a label:
<?php
$tree = new XULTree( 'single', 1,
new XULTreeCols(
new XULTreeCol( 'Name', 1 )
),
new XULTreeChildren(
new XULTreeItem( 'John' ),
new XULTreeItem( 'Mary' )
)
);
The result looks like this:
In a multi-column tree, the structure is slightly more complex:
<?php
$tree = new XULTree( 'single', 1,
new XULTreeCols(
new XULTreeCol( 'Name', 1 ),
new XULTreeCol( 'Age', 1 )
),
new XULTreeChildren(
new XULTreeItem(
new XULTreeRow(
new XULTreeCell( 'John' ),
new XULTreeCell( '31' )
)
),
new XULTreeItem(
new XULTreeRow(
new XULTreeCell( 'Mary' ),
new XULTreeCell( '27' )
)
)
)
);
Resulting in:
Columns can be made resizable by placing XULSplitters in between them. These need to have the class name
tree-splitter in order to appear correctly:
<?php
$tree = new XULTree( 'single', 1,
new XULTreeCols(
new XULTreeCol( 'Name', 1 ),
new XULSplitter( 'tree-splitter' ), // Make the cols resizable
new XULTreeCol( 'Age', 1 )
),
new XULTreeChildren(
new XULTreeItem(
new XULTreeRow(
new XULTreeCell( 'John' ),
new XULTreeCell( '31' )
)
),
new XULTreeItem(
new XULTreeRow(
new XULTreeCell( 'Mary' ),
new XULTreeCell( '27' )
)
)
)
);
Now if we want to make the tree hierarchical, it gets ever so slightly more complex:
<?php
$tree = new XULTree( 'single', 1,
new XULTreeCols(
new XULTreeCol( 'Name', 1, TRUE ),
new XULTreeCol( 'Age', 1 )
),
new XULTreeChildren(
new XULTreeItem( TRUE,
new XULTreeRow(
new XULTreeCell( 'Friends' ),
new XULTreeCell( '' )
),
new XULTreeChildren(
new XULTreeItem(
new XULTreeRow(
new XULTreeCell( 'John' ),
new XULTreeCell( '31' )
)
),
new XULTreeItem(
new XULTreeRow(
new XULTreeCell( 'Mary' ),
new XULTreeCell( '27' )
)
)
)
),
new XULTreeItem( TRUE,
new XULTreeRow(
new XULTreeCell( 'Foes' ),
new XULTreeCell( '' )
),
new XULTreeChildren(
new XULTreeItem(
new XULTreeRow(
new XULTreeCell( 'Captain Blackbeard' ),
new XULTreeCell( '58' )
)
),
new XULTreeItem(
new XULTreeRow(
new XULTreeCell( 'Stewie' ),
new XULTreeCell( '3' )
)
)
)
)
)
);
Note that in this example, the XULTreeItem constructors of the root items get a parameter 'TRUE', which indicates that the item is supposed to be a container and hold subitems. Also note the third parameter (TRUE) in the first XULTreeCol object, which indicates that this column is the
primary column. A hierarchical tree always needs one primary column, this is the column in which the twisty's are placed to expand the item and show its subitems. The resulting tree looks like this (after opening the container items):
The
XULTreeCol dynamic constructor takes these parameters:
- $label (string) - The label for the column header. This can also be left empty and replaced for an image through the XULTreeCol::src() method. The contents of the label can be changed through the Node::label() method.
- $flex (integer) - The flex value, defaults to 0 which means that the column will be as wide as the contents of the header.
- $primary (boolean) - Determines whether this is the primary column. Defaults to FALSE. Can be changed through XULTreeCol::primary().
- $dataId (string) - The name of the property or an indication of the method to call on a data object. Only applies to XULCustomTree trees.
The
XULTreeItem dynamic constructor accepts several parameters:
- $label (string) - The label for the item (this is ignored when the item contains a XULTreeRow with XULTreeCells). The contents of the label can be changed through the Node::label()
method.
- $container (boolean) - Whether the item is a container for other items. This defaults to FALSE.
- $open (boolean) - If this item is a container, this variable indicates if it should be open by default. Defaults to FALSE.
The
XULTreeCell dynamic constructor also allows several parameters:
- $label (string) - The label for the cell. The contents of the label can be changed through the Node::label()
method.
- $src (string) - An image path. This can be either a URL, or a path relative to the SiteFusion installation directory, starting with /. Suppose that your image is located in app/testapp/images/myimage.png, your $src would be "/app/testapp/images/myimage.png". Defaults to empty.
The
XULTree dynamic constructor accepts these parameters:
- $seltype (string) - This can be 'single' to allow selection of only one row, 'multiple' to allow selection of multiple rows using CTRL or Shift. Defaults to 'multiple'.
- $enableColumnDrag (boolean) - Indicates whether the column order can be changed by dragging the columns. Defaults to FALSE.
- $selstyle (string) - If set to 'primary', selecting a row will highlight only the primary column. Default is empty, which results in the entire row being highlighted.
- $flex (integer) - The flex value for the tree. Defaults to 0.
In order to get the selection from the tree, you can just supply the XULTree object as a yielder for any kind of event. After yielding, the selected rows can be found in
XULTree::$selectedRows. Changing the selection can be done through the
XULTree::select() method, which accepts either a single XULTreeItem object or an array of these objects.
As you can see the code gets progressively longer and more and more nodes are used to structure the tree. Of course, in practical situations, you would probably have a loop construction of some sort that dynamically adds items and their contained elements, but the sheer amount of nodes limits the flexibility and performance of this kind of tree. For this reason, and many others, there is the XULCustomTree class.
Complex Trees
The
XULCustomTree class was built to overcome the shortcomings of an element-based tree. It implements Mozilla's nsITreeView interface on the clientside to enable every possible feature through SiteFusion. Unlike simple trees, a XULCustomTree does not use XUL elements to build the contents of the tree, but comes with a separate data class,
TreeDataSet. The construction of a XULCustomTree is similar to a simple tree:
<?php
$tree = new XULCustomTree( 'single', 1,
new XULTreeCols(
new XULTreeCol( 'Name', 1, TRUE, 'name' ),
new XULTreeCol( 'Age', 1, 'age' )
),
new XULTreeChildren
);
$dataSet = new TreeDataSet( $tree );
The
constructor of the TreeDataSet takes two parameters:
- $tree (object XULCustomTree) - The tree for which it will provide the content
- $sparse (boolean) - Whether the tree should be filled sparsely. If TRUE, the server will send only the contents of the opened containers to the client, which makes loading large trees much faster. Defaults to FALSE.
Note how the XULTreeChildren element is present, but will not be containing any elements. It's there just to provide a virtual container for the rows that the TreeDataSet object is going to contain. Also note how the
XULTreeCol constructor can take another parameter, which is called the $dataId. This string determines which property of the row object or data object is going to be shown in that column. This will become clear when we expand this example:
<?php
$newRow = $dataSet->createRow();
$newRow->name = 'John';
$newRow->age = '31';
$dataSet->addRow( $newRow );
In this example, we create one new row and directly set the contents of the name and age cells on the row object. These properties correspond with the dataId strings supplied to the XULTreeCol constructors, and automatically change the contents of the cell when changed. Rows are created through the
TreeDataSet::createRow() method. This method takes two parameters:
- $data (array|object) - You can supply a data object or array to the row, which it will then use to take the contents for the cells from. If you don't want to use a data object, you can supply NULL which is the default. If you supply a $data parameter, it will be stored in the TreeDataRow::$data property. Note that because of this, 'data' is an invalid dataId for a XULTreeCol.
- $container (boolean) - This optional boolean indicates whether this row will be able to contain child rows. Default is FALSE.
Similarly, tree separators can be created through the
TreeDataSet::createSeparator() method. A separator can be selected too and will be included in your retrieved selections, so you need to control for this if you use them. Also, currently sorting functions fails in trees that contain separators. This is a known issue.
Every tree item, row or separator has the following properties:
As seen in the example, the adding of rows to the dataset or to each other can be done with the
TreeDataSupports::addRow() method. To insert a row before or after another row, use the methods
TreeDataSupports::insertRowBefore() and
TreeDataSupports::insertRowAfter(). These methods take the new row as the first parameter, and the reference row as the second parameter. Rows can also be removed through the method
TreeDataItem::remove(), which removes the row that the method is called on, and
TreeDataSupports::removeRow( $childRow ), which removes the given child row from the row or dataset it is called on. Both methods return the removed row, which may be discarded or for example added elsewhere in the tree.
The use of data objects enables you to use existing class structures and have them easily displayed in a XULCustomTree. Another feature in this area is that you can set the dataId of a XULTreeCol to a string ending in "()", which will cause a method to be called on the data object and the return value displayed. If you for example set the age XULTreeCol's dataId to
"getAge()", the row will call the method getAge() on the data object and display the returned value:
<?php
class Person {
public $name;
private $age;
public function __construct( $name, $age ) {
$this->name = $name;
$this->age = $age;
}
public getAge() {
return $this->age;
}
}
$john = new Person( 'John', 31 );
$myRow = $dataSet->createRow( $john );
$dataSet->addRow( $myRow );
Whereas setting the property $myRow->name to another value would change the contents of the cell in the 'name' column, setting the same property on the $john object would not. Data objects are still separated from the row contents, and the cell doesn't update when the data object is changed. To refresh the contents of the row from the data object, call the method
TreeDataRow::update(). This method can take an optional parameter $data, another data object which when given will replace the original data object.
The hierarchical tree example above would look very different in a XULCustomTree:
<?php
$myData = array(
'Friends' => array(
array( 'name' => 'John', 'age' => 31 ),
array( 'name' => 'Mary', 'age' => 27 )
),
'Foes' => array(
array( 'name' => 'Captain Blackbeard', 'age' => 58 ),
array( 'name' => 'Stewie', 'age' => 3 )
)
);
foreach ( $myData as $groupName => $items ) {
$groupItem = $dataSet->createRow( NULL, TRUE );
$groupItem->name = $groupName;
$dataSet->addRow( $groupItem );
foreach ( $items as $item ) {
$subItem = $dataSet->createRow( $item );
$groupItem->addRow( $subItem );
}
}
Here we have separated the data in a structured form, and written a nested loop that adds rows from the data arrays. A container row can be opened or closed automatically through the
TreeDataRow::open() method, which takes a boolean as its only parameter, indicating whether the row should be opened or closed. Correspondingly, you can set an event handler for the
openStateChange event on the XULCustomTree node to react to the user opening and closing a row. A handler for this event receives the openstate (boolean) and the row id (integer) as its event arguments. A row id can be resolved to the corresponding row through the
TreeDataSet::$idToRow array.
Images, Checkboxes and Progressmeters
Cells can contain either text, a checkbox or a progressmeter. In a XULCustomTree, any cell can contain an image as well, either together with other content or as its only content. In the last case, the column type is text, but the label is kept empty. The type of content for a cell is determined by its column. To set this type, use the
XULTreeCol::type() method. Three types can be set:
Column types- "text" - The cells in this column will contain text, or an image, or both.
- "checkbox" - The cells in this column will act as checkboxes. However the user will not be able to change the state of the checkbox until the tree, column and individual cells are marked as editable.
- "progressmeter" - The cells in this column will contain progressmeters.
NOTE: The type of a column must be set before a TreeDataSet is constructed and bound to it. This is because the TreeDataSet determines immediately the type of the columns and will not update this information later on.
Once the type of a column is set to for example
"checkbox", the value of the cell will be interpreted as a boolean indicating the checkbox state:
<?php
$tree = new XULCustomTree( 'single', 1,
new XULTreeCols(
$adminCol = new XULTreeCol( 'Administrator', 0, 'isAdmin' ),
$nameCol = new XULTreeCol( 'Name', 1, 'name' )
),
new XULTreeChildren
);
$adminCol->type( 'checkbox' );
$nameCol->type( 'text' ); // This is the default
$dataSet = new TreeDataSet( $tree );
$user = array( 'name' => 'John', 'isAdmin' => TRUE );
$myRow = $dataSet->createRow( $user );
$dataSet->addRow( $myRow );
The above example will result in:
Now lets add a progressmeter column, and add an image to the name column. If you have a primary column and want to show an image for the cell in that column, you can use the
TreeDataRow::primaryImage() method. However, the
TreeDataRow::setImage() method can set an image for any kind of cell:
<?php
$tree = new XULCustomTree( 'single', 1,
new XULTreeCols(
$adminCol = new XULTreeCol( 'Administrator', 0, 'isAdmin' ),
$nameCol = new XULTreeCol( 'Name', 1, 'name' ),
$progressCol = new XULTreeCol( 'Task completion', 1,
'progress' )
),
new XULTreeChildren
);
$adminCol->type( 'checkbox' );
$nameCol->type( 'text' ); // This is the default
$progressCol->type( 'progressmeter' );
$dataSet = new TreeDataSet( $tree );
$user = array(
'name' => 'John',
'isAdmin' => TRUE,
'progress' => 20
);
$myRow = $dataSet->createRow( $user );
$myRow->setImage( 'name', '/app/testapp/images/user.png' );
$myRow->progressMode( 'progress', 'normal' );
$dataSet->addRow( $myRow );
As you can see, the method
TreeDataRow::setImage() sets an image for the cell in the specified column, and the method
TreeDataRow::progressMode() must be called on a progressmeter-type column cell in order to set it to either
"normal" (like in the example, it will show the value of the cell as a progress bar), or
"undetermined" (this will draw an undetermined progressmeter). The result of this code looks like this:
Editing
Only text and checkbox cells can be edited. In order to make a cell editable, the whole tree and the appropriate columns must first be indicated as editable. This is done by calling the
XULTree::editable() and
XULTreeCol::editable() methods with a TRUE parameter.
<?php
$tree = new XULCustomTree( 'single', 1,
new XULTreeCols(
$adminCol = new XULTreeCol( 'Administrator', 0, 'isAdmin' ),
$nameCol = new XULTreeCol( 'Name', 1, 'name' )
),
new XULTreeChildren
);
$tree->editable( TRUE );
$adminCol->editable(TRUE)->type('checkbox');
$nameCol->editable(TRUE);
Then, for each individual row and cell, editable mode must be set separately:
<?php
$dataSet = new TreeDataSet( $tree );
$user = array( 'name' => 'John', 'isAdmin' => TRUE );
$myRow = $dataSet->createRow( $user );
$myRow->setEditable( 'name', TRUE );
$myRow->setEditable( 'isAdmin', TRUE );
$dataSet->addRow( $myRow );
The result being that the checkbox can now be toggled on and off, and the name cell can be text-edited:
When a cell is changed, the new value will automatically become available through the column-named property on the row object, the same property that can be used to change the contents of the cell programmatically. In the case of a checkbox this value is a boolean, and in a text cell it's a string. If you want to take a certain action when a cell is changed, you can attach an event handler to the
cellUpdated event of the XULCustomTree node:
<?php
// Set the event handler
$tree->setEventHandler( 'cellUpdated', $myHandler, 'onCellUpdated' );
// Then define the handler on $myHandler's class
public function onCellUpdated( $event, $row, $column, $value ) {
// Note that $row->{$column} == $value
// In this example, we'll update the data object
// associated to the row:
$row->data->{$column} = $value;
}
The event handler of the cellUpdated event receives several parameters:
- $event (object Event) - The event object
- $row (object TreeDataRow) - The row in which the cell was changed
- $column (string) - The column dataId for the cell that was updated
- $value (string|boolean) - The new value of the cell
Sorting
The XULCustomTree class supports several kinds of sorting, and can sort hierarchical trees recursively. A tree dataset needs to be indicated as sortable before users can click the column headers to sort on that column. This is done through the
TreeDataSet::setSortable() method. After calling this method with a TRUE parameter, the user can click the column headers once to sort in ascending order, and again to sort in descending order. Depending on the kind of data a column contains, you may need different sorting algorithms. The sorting algorithm for a certain column can be set through the
TreeDataSet::setColumnSortType() method. This method takes two parameters:
- $col (string) - The dataId of the column to set the sorting type for
- $type (string) - The sorting algorithm to apply to this column. This can be one of the following values, of which 'natural' is the default:
- "natural" - This algorithm sorts alphanumerically, but tries to keep numeric parts of the string in a logical order. This means that f.e. "file9" will appear above "file10" in ascending order because 9 is lower than 10.
- "alphanumeric" - This is the strict alphanumeric sorting algorithm. Here, "file9" will appear below "file10" in ascending order because 9 is higher than 1.
- "numeric" - Numeric ordering. All contents will be compared as floating point numbers for sorting purposes.
- "date" - This sorting algorithm tries to interpret the column contents as dates. It uses the PHP function strtotime() for that, which allows a wide variety of date formats.
Note that all sorting is case-insensitive. Sorting can also be initiated programmatically through the
TreeDataSet::sortColumn() method which takes the column dataId as the first parameter and mimics a mouseclick on the header taking the current sorting state into account. If supplied with the second optional parameter, (string,
"asc" or
"desc") it will force the sorting order to be the indicated order. If you want to sort the contents of a specific row of the tree, use the
TreeDataSupports::sort() method. An optional boolean parameter indicates whether the sorting should be done recursively. The default is FALSE. Note that sorting the tree through these methods can be done even when the dataset has not been indicated as sortable by the
TreeDataSet::setSortable()
method.
Drag and Drop
Rows in a tree can be dragged and dropped on and in between each other, or from one tree to another. Also, non-tree nodes can be dragged onto a row in a tree. However, due to a limitation in the Mozilla tree implementation, dropping rows from a tree on a non-tree node is not possible. When Mozilla enables this, XULCustomTree will also implement this possibility.
Rows can be dragged when they are indicated as draggable with the
TreeDataRow::setDraggable() method, or when the whole dataset is declared draggable through
TreeDataSet::setDraggable(). If the dataset is set to draggable, individual rows can still be made undraggable or indicate a different action, because calls to TreeDataRow::setDraggable()
override the TreeDataSet::setDraggable() setting. These methods accept two parameters:
- $allow (boolean) - Whether to allow dragging
- $type (string) - The type of operation that the mouse cursor should indicate
- "move" - Indicate a move action
- "copy" - Indicate a copy action
- "link" - Indicate a link or shortcut action
- "moveOrCopy" - Holding the shift key will alternate between move and copy
Row dropping policy is likewise determined by
TreeDataSet::allowDrop() and
TreeDataRow::allowDrop(). These methods take two parameters as well:
- $allow (boolean) - Whether to allow dropping of rows/nodes on this row
- $type (string) - The type of dropping allowed
- "on" - Dropping is only possible when the item is dragged on the row
- "between" - Dropping is only possible between items, above and below
- "any" - Dropping is possible everywhere
NOTE: Only rows that are containers can accept dragged content in the "on" fashion. This is a Mozilla limitation.Initially, a tree only allows dropped rows when they originate from itself. To allow rows from other trees, or other draggable nodes, to be dropped in the tree, use
TreeDataSet::allowForeignDrop(). It takes only the parameter
$allow (boolean).
When a tree row is dropped in a tree, the tree fires the
treeItemDropped event. Note that this is a local event, so trying to set an event message type will fail. Handlers for this event will receive these parameters:
- $event (object Event) - The event object
- $sourceTree (object XULCustomTree) - The tree from which the row(s) originate
- $rows (array [object TreeDataRow]) - Array of dragged rows
- $targetRow (object TreeDataRow) - The row that the drop was performed on
- $orientation (integer) - The type of drop: -1 = above target row, 0 = on target row, 1 = under target row
Below is an example of a handler for this event:
<?php
// Set the event handler
$tree->setEventHandler( 'treeItemDropped', $myHandler,
'onTreeItemDropped' );
// Define the handler method on $myHandler's class
public function onTreeItemDropped( $event, $sourceTree, $rows,
$targetRow, $orientation ) {
// Remove rows from original position
foreach ( $rows as $row ) {
$row->remove();
}
// If the rows are dropped on the targetRow, add as
// child, otherwise insert in between rows
switch ( $orientation ) {
case 0:
foreach ( $rows as $row ) {
$targetRow->addRow($row);
}
break;
case -1:
foreach ( $rows as $row ) {
$targetRow->parentRow->insertRowBefore( $row,
$targetRow);
}
break;
case 1:
foreach ( $rows as $row ) {
$targetRow->parentRow->insertRowAfter( $row,
$targetRow);
}
break;
}
}
When other draggable nodes are dropped on a tree, it fires the
nodeDropped event. Note that this is a local event, so trying to set an event message type will
fail. The event handler receives the following parameters:
- $event (object Event) - The event object
- $node (object Node) - The dragged node
- $targetRow (object TreeDataRow) - The row that the drop was
performed on
- $orientation (integer) - The type of drop: -1 = above target
row, 0 = on target row, 1 = under target row