Tree Widget

Last modified by Admin on 2024/10/28 17:39

chart_organisationA tree widget based on jsTree
Typewebjar
CategoryWebJar
Developed by

XWiki Development Team

Rating
0 Votes
LicenseGNU Lesser General Public License 2.1
Bundled With

XWiki Standard

Compatibility

6.3RC1+

Description

tree.png The tree widget is based on jsTree and thus it provides all the features supported by jsTree plus a few extra. You can create both static and dynamic (interactive) trees where the tree structure and behaviour is defined in a wiki page. It is written as a jQuery plugin that extends the jsTree plugin adding a few extra APIs to ease the integration with XWiki.

Usage

Maven Dependency

In order to use the tree widget in your extension the first step would probably be to add the tree widget as a dependency.

<dependency>
 <groupId>org.xwiki.platform</groupId>
 <artifactId>xwiki-platform-tree-webjar</artifactId>
 <version>6.3-rc-1</version>
</dependency>

Of course, you can change the version. Note that the tree widget is packaged as a WebJar so you should also add a dependency to the WebJar integration module.

RequireJS Configuration

Next, you need to load the tree widget. The recommended way is through RequireJS. Here's an example configuration:

require.config({
  paths: {
    jsTree: "$!services.webjars.url('jstree', 'jstree.min.js')",
    JobRunner: "$!services.webjars.url('org.xwiki.platform:xwiki-platform-job-webjar', 'jobRunner.min.js')",
    tree: "$!services.webjars.url('org.xwiki.platform:xwiki-platform-tree-webjar', 'tree.min.js')"
  },
  shim: {
    jsTree: {
      deps: ['jquery']
    }
  }
});

Then you can create a tree:

require(['tree'], function($) {
  $('.someClass').xtree();
});

The difference from creating the tree with jsTree is that you have to call 'xtree' instead of 'jstree' but otherwise it will behave the same. As with jsTree, you can pass a configuration object. You can put this code (including the RequireJS configuration) in a JavaScriptExtension object for instance. 

Besides the JavaScript code, you also have to load the CSS. You can obtain the URL like this:

$services.webjars.url('org.xwiki.platform:xwiki-platform-tree-webjar', 'tree.min.css', {'evaluate': true})

Note that the CSS is evaluated because it uses Color Theme variables (i.e. Velocity code). You can load the CSS statically, or dynamically with a bit of JavaScript help:

(function loadCss(url) {
 var link = document.createElement("link");
  link.type = "text/css";
  link.rel = "stylesheet";
  link.href = url;
 document.getElementsByTagName("head")[0].appendChild(link);
})("$services.webjars.url('org.xwiki.platform:xwiki-platform-tree-webjar', 'tree.min.css', {'evaluate': true})");

Configuration

From JavaScript

When you create a tree you can pass a configuration object:

$('.someClass').xtree({
  core: {
    themes: {
      icons: false
    }
  },
  plugins: ['dnd', 'contextmenu', 'checkbox'],
  dnd: {
    ...
  }
});

See the jsTree documentation for all the available options.

From HTML

You can also configure the tree from the HTML using 'data' attributes:

<div class="myTree" data-url="$xwiki.getURL('Space.TreeStructure', 'get', 'outputSyntax=plain')"
 data-responsive="true" data-finder="true" ... />
Data AttributeDescriptionDefault Value
checkboxesEnables the checkbox pluginfalse
contextMenuEnables the context menu pluginfalse
dragAndDropEnables the drag&drop pluginfalse
edgesSee core.themes.dotstrue
finderEnables the finder pluginfalse
iconsSee core.themes.iconstrue
responsiveSee core.themes.responsivetrue
rootThe id of the root node. This is useful if you want to display a sub-tree that starts from a given node. It works only for dynamic or lazy-loaded trees, i.e. when the url parameter is specified. Starting with version 7.2RC1 you should use the url parameter instead, passing the root node id in the root query string parameter (e.g. outputSyntax=plain&root=someNodeId).None
urlThe URL used to load the tree nodes, and most of the information related to the tree (context menu, path of a specified node). If not specified then the tree widget tries to create a static tree from the content of the target HTML elementNone

You can also use a special CSS class 'jstree-no-links' to disable default link behaviour (i.e. clicking on a node will select the node instead of following the link). You can set this CSS class on the tree element to affect the entire tree or on a tree node element to affect that node and its descendants. If you want to disable the default link behaviour for a specific tree node without affecting its child nodes then you can use the 'jstree-no-link' CSS class instead (7.4M2+), which can also be set from JSON, as shown in the Advanced Node Data section below.

API

In order to get a reference to the tree object you can use the standard jsTree API:

var tree = $.jstree.reference($('.myTree'))

Then you can call any method provided by jsTree and a few more extra:

  • Open the tree to a specified node, that is not necessarily loaded:
    tree.openTo('someNodeId', function(node) {
      console.log('done!');
    });

    The tree will ask the server side for the node path and it will open all the ancestors of the specified node. If the tree is paginated and one of the ancestors is not on the first 'page' of its parent (when the parent is expanded) then the ancestor will be added to the parent (since loading all the 'pages' would be too expensive). This doesn't affect the pagination, as when the 'page' that actually contains the ancestor is retrieved, the ancestor won't be added again to the tree.

    If you don't pass any callback function (as the second parameter) then the tree will select the specified node. Otherwise, if you pass a callback function, you'll have to select the node yourself (in the callback) if you wish to. Note that the node passed to the callback function is the last node from the path that could be opened. So this is not necessarily the node you wanted. You can check its id if you want to make sure.

  • Refresh a node:
    tree.refreshNode('someNodeId');

    The difference from jsTree's 'refresh_node' is that this also works for the root node.

  • Execute an action on a node:
    var targetNode = tree.get_node('nodeId');
    var actionName = 'pack';
    var actionParams = {
     'format': 'xar'
    };
    var promise = tree.execute(actionName, targetNode, actionParams);

    This will call the server side specifying the target node, the action to execute and its parameters. You can use the returned promise to get notified of the progress. Note that the server side can even perform the action using (asynchronous) jobs. The returned promise will behave as expected as long as the server side returns the job status (partially) serialized as JSON.

Events

The events are triggered on the tree element so you can use jQuery listen to them like this:

$('.myTree').on('someEvent', function (event, data) {
 // Do something.
})

Besides the standard jsTree events, the following events are specific to the tree widget:

NameDescription
xtree.openContextMenuUseful if you want to disable some menu items before the menu is shown. The event data has 3 properties: tree, node and menu.
xtree.contextMenu.*Triggered when a context menu item is clicked. Replace "*" with the context menu action name. The event data is what you would expect from a jsTree context menu action plus a 'parameters' property that can be set from the context menu definition.

Tree Data

Static Data

Static trees can be defined using nested unordered lists inside the target HTML element:

<div class="myStaticTree" data-icons="true" data-responsive="true">
  <ul>
    <li>
      Parent
      <ul>
        <li>Child</li>
      </ul>
    </li>
  </ul>
</div>

See the jsTree's HTML data source for more information.

Dynamic Data

Dynamic tree data is loaded using the url data attribute.

<div class="myDynamicTree" data-url="put/your/url/here" data-contextMenu="true"
 data-dragAndDrop="true" data-icons="true" data-edges="true" data-responsive="true" />

The resource behind the specified URL must implement the following API (contract):

  • Request some data
    • ?data=children&id=<parentNodeId>&offset=<offset>&showRoot=<true|false>&root=<rootNodeId>

      Requests the children of the specified parent node from the specified offset. The resource decides how many child nodes to return. The resource is expected to implement the following behaviour:

      • if id equals '#' then the top level nodes in the tree are requested (i.e. the first level in the tree)
        • if showRoot=true then return information about the specified root node or about the default root node in your tree model
        • else return information about the children of the specified root node or the children of the default root node in your tree model
      • else return information about the children of the specified tree node

      The response is a JSON array with node data. See jsTree's JSON data source for more information.

    • ?data=path&id=<nodeId>&root=<rootNodeId>

      Requests the path of the specified node. The response is a JSON array that contains data about all the ancestors of the specified node (including itself) starting from the specified root node or from the default root node in your tree model. The response is similar to the one returned when the list of child nodes is requested, only that this time the list of ancestors is returned.

    • ?data=contextmenu

      Requests the tree context menu. The response is a JSON object (map) where the key is the node type and the value is the context menu for that node type. See jsTree's context menu definition for more information.

    • ?data=jobStatus&id=<jobId>

      Requests the status for the specified job. This is needed when a tree action is performed using asynchronous jobs. The response is a JSON object that looks like this:

      {
       'id': $jobId,
       'state': $jobStatus.state,
       'request': {
         'type': $jobStatus.request.getProperty('job.type'),
         'user': "$jobStatus.request.getProperty('user.reference')"
        },
       'progress': {
         'offset': $jobStatus.progress.offset,
         'currentLevelOffset': $jobStatus.progress.currentLevelOffset
        },
       'startDate': $jobStatus.startDate,
       'endDate': $jobStatus.endDate
      }

      As you can notice the response is a partial serialization of the job status. You can add more data depending on the performed action, of course.

  • Request an action: ?action=<actionName>. The action can be performed both synchronous and asynchronous. For synchronous actions it's enough to return the proper HTTP status code for success and failure. For asynchronous actions (e.g. a job running in background in a separate thread) you may want to return the job status as JSON.

If you want to create your own dynamic tree then follow the Creating a Tree View tutorial. For advanced examples of dynamic trees please checkout:

Advanced Node Data

Besides what is supported by jsTree's JSON format for node data, the tree widget also supports this:

{
 // The node id
 'id': 'document:xwiki:Main.WebHome',
  ...
 'data': {
   // The id of the entity represented by this node
   'id': 'xwiki:Main.WebHome',
   // The node/entity type
   'type': 'document',
   // The type of nodes that can be children of this node
   'validChildren': ['translations', 'attachment', 'document', 'pagination'],
   // Whether this node has a context menu or not (when you right click on it)
   'hasContextMenu': true,
   // Whether you can drag & drop this node
   'draggable': true,
   // Whether this node can be deleted by the current user
   'canDelete': false,
   // Whether this node can be renamed by the current user
   'canRename': false,
   // Whether this node can be moved by the current user
   'canMove': true,
   // Whether the current user can copy this node
   'canCopy': true,
   // Custom action URL. This URL will be used instead of the tree source URL with ?action=delete
   'deleteURL': $docNode.getURL('delete', 'confirm=1')
  },
  ...
}

If drag&drop is enabled then the tree widget will prevent moving nodes in the wrong place based on the node type and its valid children. The can* properties are used to enable/disable standard operations on tree nodes. The custom action URL can be used for standard actions (e.g. delete, move, copy) but also for custom actions (from the context menu).

Starting with XWiki 7.4M2 you can also disable the default link behaviour for a specific tree node without affecting its children, by using the 'jstree-no-link' CSS class like this:

{
  ... (node JSON) ...
  'a_attr': {
    'class': 'jstree-no-link',
    'href': 'some/url'
  }
}

Pagination

The tree widget supports a special node type 'pagination':

{
 'id': "pagination:putParentIdHere",
 'text': "36 more ...",
 'icon': 'fa fa-eye',
 'children': false,
 'data': {
   'type': 'pagination',
   'validChildren': [],
   'canDelete': true,
   'offset': 15
  }
}

When the pagination node is clicked the tree makes a request to get the children of the pagination parent node, from the specified offset, and replaces the pagination node with the response. Obviously the response can contain another pagination node that will be used to get the next 'page'. The pagination node is usually the last child in the response.

Context Menu

jsTree provides support for creating context menus but you need to specify the menu structure on the client side. The tree widget adds support for retrieving the context menu structure from the server in JSON format. Since you cannot pass a function in JSON you need to specify the menu actions (what happens when a menu is clicked) a bit differently.

If you have a menu like this in jsTree:

{
  ...
 'export': {
   'label': 'Export as XAR',
   'icon': 'fa fa-download',
   'action': function(data) {
     // Do something
   }
  }
  ...
}

this is how you would write it for the tree widget:

// The JSON that defines the context menu, returned by the server
{
  ...
 'export': {
   'label': 'Export as XAR',
   'icon': 'fa fa-download',
   'action': 'export'
  }
  ...
}

// On the client side you listen to an event:
$('.myTree').on('xtree.contextMenu.export', function(event, data) {
 // Do something
})

If the action name is the same as the menu item id (key) then you can omit the action name as in:

{
 'export': {
   'label': 'Export as XAR',
   'icon': 'fa fa-download'
  }
}

Note that menu actions can have parameters so you can write something like this:

// The context menu JSON
{
  ...
 'exportAsXAR': {
   'label': 'Export as XAR',
   'icon': 'fa fa-download',
   'action': 'export',
   'parameters': {
     'format': 'xar'
    }
  }
  ...
}

// The context menu item action
$('.myTree').on('xtree.contextMenu.export', function(event, data) {
 if (data.parameters.format == 'xar') {
   // Export as XAR
 } else {
   // Do something else
 }
})

Note that this way you can have multiple menu items that reuse the same action, but with different parameters.

Default menu items

The following menu items are provided by default:

Menu key / ActionDescription
copyMarks the selected nodes as copied. Use the paste action to perform the actual copy.
createCreate a new node under the target node.
cutMarks the selected nodes as cut. Use the paste action to perform the actual move.
openLinkOpens the link of the target node in the current browser window/tab. The URL used is by default the value of the 'href' attribute of the node's anchor element (see the 'a_attr' node configuration option). You can specify a different URL using the 'urlProperty' parameter. The value of this parameter is the name of a property from the node's data that holds the URL. You can have for instance:
// Node JSON
{
 'id': 'foo',
  ...
 'data': {
   'type': 'bar',
    ...
   'downloadURL': 'url/to/download'
  },
 'a_attr': {
   'href': 'some/url/that/you/want/to/overwrite'
  }
}

// Context Menu JSON
{
  ...
 'bar': {
    ...
   'download': {
     'label': 'Download',
     'icon': 'fa fa-download',
     'parameters': {
       'urlProperty': 'downloadURL'
      }
    },
    ...
  },
  ...
}
openLinkInNewTabSame as openLink but in a new browser window/tab
pastePerforms the actual copy or move, depending on the last copy/cut action
refreshReloads the children of the target node
removeRemoves the selected nodes
renameRenames the target node

Finder

Starting with version 6.4.1 the tree widget has a (jsTree) plugin to find tree nodes, called finder. When enabled, this plugin adds a text input above the tree that offers suggestions as you type based on the content of the tree. When such a suggestion is selected the tree is expanded to show the corresponding node.

You enable and configure the finder plugin like any other jsTree plugin:

$('.someClass').xtree({
  ...
  plugins: [..., 'finder'],
  finder: {
   // Finder configuration parameters here.
   //
   // The service that provides the finder suggestions as a JSON array. The suggestion format is the same as for the
   // node JSON data, with a few more properties. See the 'Advanced Node Data' section for details. If not specified
   // then the data URL of the tree is used, with data=suggestions in the query string.
   url: '',
   // The text input placeholder.
   placeholder: 'find ...',
   // Specify which properties from the suggestion JSON response should be used for display.
   suggestion: {
     // Additional information to be displayed after the node label, on the same line (e.g. child count).
     hint: 'data.hint',
     // Additional information to be displayed below the node label (e.g. the node path).
     info: 'data.info',
     // The suggestion/node type. This is useful when the tree contains multiple types of nodes and you want to style
     // each suggestion differently based on the corresponding node type.
     type: 'data.type'
    }
  }
  ...
});

Examples

The tree widget is currently used by:

Static Tree

Create a new page Test.StaticTree with the following content:

{{velocity}}
{{html}}
#set ($discard = $xwiki.linkx.use($services.webjars.url('org.xwiki.platform:xwiki-platform-tree-webjar', 'tree.min.css',
  {'evaluate': true}), {'type': 'text/css', 'rel': 'stylesheet'}))
#set ($discard = $xwiki.jsx.use('Test.StaticTree'))
<div class="myCustomTree">
  <ul>
    <li><a>Group 1</a>
      <ul>
        <li><a>SubGroup 1</a></li>
        <ul>
          <li><a>Item 11</a></li>
          <li><a>Item 12</a></li>
          <li><a>Item 13</a></li>
        </ul>
        <li><a>SubGroup 2</a></li>
        <li><a>Item 21</a></li>
        <li><a>Item 22</a></li>
      </ul>
    </li>
    <li>
      <a>SubGroup 3</a>
      <ul>
        <li><a>Item 31</a></li>
        <li><a>Item 32</a></li>
      </ul>
    </li>
    <li>
      <a>Item 4</a>
    </li>
  </ul>
</div>
{{/html}}
{{/velocity}}

Then add a JavaScriptObject with this content to the same page:

require(["$!services.webjars.url('org.xwiki.platform:xwiki-platform-tree-webjar', 'require-config.min.js', {'evaluate': true})"], function() {
  require(['tree'], function($) {
    $('.myCustomTree').xtree({
      core: {
        themes: {
          icons: false,
          dots: false
        }
      },
      plugins: ['checkbox'],
      checkbox: {
        three_state: false
      }
    });
  });
});

Make sure you set the "Parse content" property to Yes, then save and view the page. Check the jsTree documentation for more configuration options.

Dependencies

Dependencies for this extension (org.xwiki.platform:xwiki-platform-tree-webjar 16.9.0):

Get Connected