diff --git a/xwiki-platform-core/pom.xml b/xwiki-platform-core/pom.xml
index 8af028369c3cdcf7b6e73fb158ce74ea47350070..b009207e54ea52c2439a056a08ae89c68d11c508 100644
--- a/xwiki-platform-core/pom.xml
+++ b/xwiki-platform-core/pom.xml
@@ -37,6 +37,8 @@
     <!-- JS/CSS minification is on by default, this property is used in order to able to skip minification when the
          debug profile is active. It can also be used from the command line to skip minification -->
     <xwiki.minification.skip>false</xwiki.minification.skip>
+    <!-- Follow the specifications regarding the WebJar content path. -->
+    <webjar.contentDirectory>${project.build.outputDirectory}/META-INF/resources/webjars/${project.artifactId}/${project.version}</webjar.contentDirectory>
   </properties>
   <dependencies>
     <dependency>
@@ -216,6 +218,13 @@
         <artifactId>joda-time</artifactId>
         <version>2.5</version>
       </dependency>
+      <!-- Common WebJars -->
+      <dependency>
+        <groupId>org.webjars</groupId>
+        <artifactId>jquery</artifactId>
+        <version>1.11.1</version>
+        <scope>runtime</scope>
+      </dependency>
     </dependencies>
   </dependencyManagement>
   <build>
@@ -305,6 +314,21 @@
           <groupId>net.alchim31.maven</groupId>
           <artifactId>yuicompressor-maven-plugin</artifactId>
           <version>1.3.2</version>
+          <executions>
+            <execution>
+              <!-- Minify the WebJar resources before packing the jar. -->
+              <id>minify-webjar-resources</id>
+              <goals>
+                <goal>compress</goal>
+              </goals>
+              <configuration>
+                <outputDirectory>${webjar.contentDirectory}</outputDirectory>
+                <!-- Don't overwrite the original version. -->
+                <nosuffix>false</nosuffix>
+                <suffix>.min</suffix>
+              </configuration>
+            </execution>
+          </executions>
           <configuration>
             <skip>${xwiki.minification.skip}</skip>
             <jswarn>false</jswarn>
@@ -312,6 +336,27 @@
             <nosuffix>true</nosuffix>
           </configuration>
         </plugin>
+        <plugin>
+          <artifactId>maven-resources-plugin</artifactId>
+          <executions>
+            <execution>
+              <!-- Copy the WebJar resources to the right path before packing the jar. -->
+              <id>copy-webjar-resources</id>
+              <phase>validate</phase>
+              <goals>
+                <goal>copy-resources</goal>
+              </goals>
+              <configuration>
+                <outputDirectory>${webjar.contentDirectory}</outputDirectory>
+                <resources>
+                  <resource>
+                    <directory>src/main/resources</directory>
+                  </resource>
+                </resources>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
       </plugins>
     </pluginManagement>
     <plugins>
@@ -419,6 +464,7 @@
     <module>xwiki-platform-invitation</module>
     <module>xwiki-platform-ircbot</module>
     <module>xwiki-platform-jira</module>
+    <module>xwiki-platform-job</module>
     <module>xwiki-platform-jodatime</module>
     <module>xwiki-platform-ldap</module>
     <module>xwiki-platform-lesscss</module>
@@ -453,6 +499,7 @@
     <module>xwiki-platform-store</module>
     <module>xwiki-platform-tag</module>
     <module>xwiki-platform-test</module>
+    <module>xwiki-platform-tree</module>
     <module>xwiki-platform-user</module>
     <module>xwiki-platform-uiextension</module>
     <module>xwiki-platform-url</module>
diff --git a/xwiki-platform-core/xwiki-platform-job/pom.xml b/xwiki-platform-core/xwiki-platform-job/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..52351da12a9e88ba3f4caef275388efa0c94d14d
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-job/pom.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.xwiki.platform</groupId>
+    <artifactId>xwiki-platform-core</artifactId>
+    <version>6.3-SNAPSHOT</version>
+  </parent>
+  <artifactId>xwiki-platform-job</artifactId>
+  <name>XWiki Platform - Job - Parent POM</name>
+  <packaging>pom</packaging>
+  <description>XWiki Platform - Job - Parent POM</description>
+  <modules>
+    <module>xwiki-platform-job-web</module>
+  </modules>
+</project>
diff --git a/xwiki-platform-core/xwiki-platform-job/xwiki-platform-job-web/pom.xml b/xwiki-platform-core/xwiki-platform-job/xwiki-platform-job-web/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..91506cbb326d36b9db995e3e2f0b9b8b8e9e67df
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-job/xwiki-platform-job-web/pom.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.xwiki.platform</groupId>
+    <artifactId>xwiki-platform-job</artifactId>
+    <version>6.3-SNAPSHOT</version>
+  </parent>
+  <packaging>jar</packaging>
+  <artifactId>xwiki-platform-job-web</artifactId>
+  <name>XWiki Platform - Job - Web</name>
+  <description>JavaScript API that can be used to watch the progress of a job</description>
+  <properties>
+    <!-- Name to display by the Extension Manager -->
+    <xwiki.extension.name>Job Web</xwiki.extension.name>
+    <!-- No Java code here -->
+    <xwiki.clirr.skip>true</xwiki.clirr.skip>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>org.webjars</groupId>
+      <artifactId>jquery</artifactId>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>copy-webjar-resources</id>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>net.alchim31.maven</groupId>
+        <artifactId>yuicompressor-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>minify-webjar-resources</id>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <includes>
+            <!-- Include only the WebJar content -->
+            <include>META-INF/**</include>
+          </includes>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/xwiki-platform-core/xwiki-platform-job/xwiki-platform-job-web/src/main/resources/jobRunner.js b/xwiki-platform-core/xwiki-platform-job/xwiki-platform-job-web/src/main/resources/jobRunner.js
new file mode 100644
index 0000000000000000000000000000000000000000..9fbd1fc148b4252f7ccd19d3c11202bb8d31f412
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-job/xwiki-platform-job-web/src/main/resources/jobRunner.js
@@ -0,0 +1,66 @@
+define(['jquery'], function($) {
+  var createCallback = function(config, promise) {
+    var answerJobQuestion = function(data) {
+      // 'this' is the job status.
+      var request = config.createAnswerRequest(this.id, data);
+      $.post(request.url, request.data).done(onProgress).fail(onFailure);
+    };
+
+    var onFailure = $.proxy(promise.reject, promise);
+
+    var onProgress = function(job) {
+      if (job && job.id && job.state && job.progress) {
+        if (job.state == 'WAITING') {
+          promise.notify(job, $.proxy(answerJobQuestion, job));
+        } else {
+          // Even if the job is finished we still need to notify the last progress update.
+          promise.notify(job);
+          if (job.state == 'FINISHED') {
+            promise.resolve(job);
+          } else {
+            // The job is still running. Wait before asking for a job status update.
+            setTimeout(function() {
+              var request = config.createStatusRequest(job.id);
+              $.get(request.url, request.data).done(onProgress).fail(onFailure);
+            }, config.updateInterval || 1000);
+          }
+        }
+      } else {
+        promise.resolve(job);
+      }
+    };
+
+    return {
+      answerJobQuestion: answerJobQuestion,
+      onFailure: onFailure,
+      onProgress: onProgress
+    };
+  };
+
+  /**
+   * Configuration object:
+   * {
+   *   createStatusRequest: function(jobId) {},
+   *   createAnswerRequest: function(jobId, data) {},
+   *   updateInterval: 1000 (in milliseconds)
+   * }
+   */
+  return function(config) {
+    this.resume = function(jobId) {
+      var promise = $.Deferred();
+      var callback = createCallback(config, promise);
+      var request = config.createStatusRequest(jobId);
+      $.get(request.url, request.data).done(callback.onProgress).fail(callback.onFailure);
+      return promise;
+    };
+
+    this.run = function(url, data) {
+      var promise = $.Deferred();
+      var callback = createCallback(config, promise);
+      $.post(url, data).done(callback.onProgress).fail(callback.onFailure);
+      return promise;
+    };
+
+    return this;
+  }
+});
diff --git a/xwiki-platform-core/xwiki-platform-tree/pom.xml b/xwiki-platform-core/xwiki-platform-tree/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a178b077ca34548157112de4726307e3d27da9ee
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-tree/pom.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.xwiki.platform</groupId>
+    <artifactId>xwiki-platform-core</artifactId>
+    <version>6.3-SNAPSHOT</version>
+  </parent>
+  <artifactId>xwiki-platform-tree</artifactId>
+  <name>XWiki Platform - Tree - Parent POM</name>
+  <packaging>pom</packaging>
+  <description>XWiki Platform - Tree - Parent POM</description>
+  <modules>
+    <module>xwiki-platform-tree-macro</module>
+    <module>xwiki-platform-tree-widget</module>
+  </modules>
+</project>
diff --git a/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-macro/pom.xml b/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-macro/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..8e172ea20fe0159f13cb3120e333f6a1e88e964d
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-macro/pom.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.xwiki.platform</groupId>
+    <artifactId>xwiki-platform-tree</artifactId>
+    <version>6.3-SNAPSHOT</version>
+  </parent>
+  <artifactId>xwiki-platform-tree-macro</artifactId>
+  <name>XWiki Platform - Tree - Macro</name>
+  <description>Tree macro</description>
+  <dependencies>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-tree-widget</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.rendering</groupId>
+      <artifactId>xwiki-rendering-transformation-macro</artifactId>
+      <version>${rendering.version}</version>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/pom.xml b/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/pom.xml
new file mode 100644
index 0000000000000000000000000000000000000000..cf9a0235af01e8ed1dae5a593e5970e76470cc27
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/pom.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.xwiki.platform</groupId>
+    <artifactId>xwiki-platform-tree</artifactId>
+    <version>6.3-SNAPSHOT</version>
+  </parent>
+  <packaging>jar</packaging>
+  <artifactId>xwiki-platform-tree-widget</artifactId>
+  <name>XWiki Platform - Tree - Widget</name>
+  <description>A tree widget based on jsTree</description>
+  <properties>
+    <!-- Name to display by the Extension Manager -->
+    <xwiki.extension.name>Tree Widget</xwiki.extension.name>
+    <!-- No Java code here -->
+    <xwiki.clirr.skip>true</xwiki.clirr.skip>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>org.webjars</groupId>
+      <artifactId>jquery</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.webjars</groupId>
+      <artifactId>jstree</artifactId>
+      <version>3.0.4</version>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-job-web</artifactId>
+      <version>${project.version}</version>
+      <scope>runtime</scope>
+    </dependency>
+  </dependencies>
+  <build>
+    <plugins>
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>copy-webjar-resources</id>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>net.alchim31.maven</groupId>
+        <artifactId>yuicompressor-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>minify-webjar-resources</id>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <includes>
+            <!-- Include only the WebJar content -->
+            <include>META-INF/**</include>
+          </includes>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/src/main/resources/tree.css b/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/src/main/resources/tree.css
new file mode 100644
index 0000000000000000000000000000000000000000..735426058a8e922d807d67140d9d446e5821dc93
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/src/main/resources/tree.css
@@ -0,0 +1,11 @@
+.jstree ul.jstree-children {
+ /* Overwrite the margin from the XWiki skin. */
+  margin: 0;
+}
+.jstree-contextmenu {
+  z-index: 100;
+}
+.jstree-contextmenu a * {
+  /* Overwrite the line height from the XWiki skin. */
+  line-height: inherit;
+}
diff --git a/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/src/main/resources/tree.js b/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/src/main/resources/tree.js
new file mode 100644
index 0000000000000000000000000000000000000000..e206b792f13c900954cbc5a156f3b8d3ac109248
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-tree/xwiki-platform-tree-widget/src/main/resources/tree.js
@@ -0,0 +1,492 @@
+require(['jquery', 'JobRunner', 'jsTree'], function($, JobRunner) {
+  var formToken = $('meta[name=form_token]').attr('content');
+
+  var getNodeTypes = function(nodes) {
+    var typesMap = {};
+    $.each(nodes, function() {
+      if (this.data && typeof this.data.type == 'string') {
+        typesMap[this.data.type] = true;
+      }
+    });
+    var types = [];
+    for (var type in typesMap) {
+      typesMap.hasOwnProperty(type) && types.push(type);
+    }
+    return types;
+  };
+
+  var getChildren = function(node, callback, parameters) {
+    // 'this' is the tree instance.
+    callback = $.proxy(callback, this);
+    if (node.id == '#' && !node.data) {
+      // If the root node doesn't have any data then infer it from its children.
+      var nestedCallback = callback;
+      callback = function(children) {
+        var validChildren = getNodeTypes(children);
+        if (validChildren.length > 0) {
+          node.data = {validChildren: validChildren};
+        }
+        nestedCallback(children);
+      }
+    }
+    var childrenURL = node.data && node.data.childrenURL;
+    parameters = parameters || {};
+    if (!childrenURL) {
+      childrenURL = this.element.attr('data-url');
+      parameters = $.extend({
+        data: 'children',
+        id: node.id
+      }, parameters);
+    }
+    if (childrenURL) {
+      $.get(childrenURL, parameters)
+        .done(callback)
+        .fail(function() {
+          callback([]);
+        });
+    } else {
+      callback([]);
+    }
+  };
+
+  var canAcceptChild = function(parent, child) {
+    return !parent.data || !parent.data.validChildren
+      || (child.data && parent.data.validChildren.indexOf(child.data.type) >= 0);
+  };
+
+  var canPerformBatchOperation = function(nodes, action) {
+    for (var i = 0; i < nodes.length; i++) {
+      var node = nodes[i];
+      if (!node.data || !node.data['can' + action]) {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  var canCopyNodes = function(nodes) {return canPerformBatchOperation(nodes, 'Copy')};
+  var canCutNodes = function(nodes) {return canPerformBatchOperation(nodes, 'Move')};
+  var canRemoveNodes = function(nodes) {return canPerformBatchOperation(nodes, 'Delete')};
+
+  var validateOperation = function (operation, node, parent, position, more) {
+    // The operation can be 'create_node', 'rename_node', 'delete_node', 'move_node' or 'copy_node'.
+    // In case the operation is 'rename_node' the position is filled with the new node name.
+    return (operation == 'create_node' && canAcceptChild(parent, node))
+      || (operation == 'rename_node' && node.data && node.data.canRename)
+      || (operation == 'delete_node' && node.data && node.data.canDelete)
+      || (operation == 'move_node' && node.data && node.data.canMove && canAcceptChild(parent, node))
+      || (operation == 'copy_node' && node.data && node.data.canCopy && canAcceptChild(parent, node));
+  };
+
+  var areDraggable = function(nodes) {
+    for (var i = 0; i < nodes.length; i++) {
+      var node = nodes[i];
+      if (!node.data || !node.data.draggable) {
+        return false;
+      }
+    }
+    return true;
+  };
+
+  var addMoreChildren = function(tree, paginationNode) {
+    // Mark the pagination node as loading to prevent multiple pagination requests for the same offset.
+    var paginationElement = tree.get_node(paginationNode.id, true);
+    if (paginationElement.hasClass('jstree-loading')) return;
+    paginationElement.addClass('jstree-loading');
+    // Replace the pagination node with the nodes from the next page.
+    var parent = tree.get_node(paginationNode.parent);
+    getChildren.call(tree, parent, function(children) {
+      var position = paginationElement.parent().children().index(paginationElement[0]);
+      tree.delete_node(paginationNode);
+      $.each(children, function(index) {
+        tree.create_node(parent, this, position + index, index == 0 && function(firstChild) {
+          tree.select_node(firstChild);
+        });
+      });
+    }, {offset: paginationNode.data.offset});
+  };
+
+  var disableNodeBeforeLoading = function(tree, node) {
+    tree.get_node(node, true).addClass('jstree-loading');
+    tree.disable_node(node);
+  };
+
+  var enableNodeAfterLoading = function(tree, node) {
+    tree.get_node(node, true).removeClass('jstree-loading');
+    tree.enable_node(node);
+  };
+
+  var createJobRunner = function(treeElement) {
+    var jobServiceURL = $(treeElement).attr('data-url');
+    return new JobRunner({
+      createStatusRequest: function(jobId) {
+        return {
+          url: jobServiceURL,
+          data: {
+            id: jobId,
+            data: 'jobStatus'
+          }
+        };
+      },
+      createAnswerRequest: function(jobId, data) {
+        return {
+          url: jobServiceURL,
+          data: $.extend({}, data, {
+            id: jobId,
+            action: 'answer',
+            form_token: formToken
+          })
+        };
+      }
+    });
+  };
+
+  var createEntity = function(tree, node) {
+    var params = {name: node.text};
+    if (node.data && node.data.type) {
+      params.type = node.data.type;
+    }
+    return tree.execute('create', tree.get_node(node.parent), params);
+  };
+
+  var deleteEntity = function(tree, node) {
+    return tree.execute('delete', node);
+  };
+
+  var moveEntity = function(tree, node) {
+    return tree.execute('move', node, {
+      parent: node.parent,
+      name: node.text
+    });
+  };
+
+  var copyEntity = function(tree, node, newParent) {
+    return tree.execute('copy', node, {parent: newParent});
+  };
+
+  var getContextMenuItems = function(node, callback) {
+    if (!node.data || !node.data.hasContextMenu) return;
+
+    var tree = this;
+    var callbackWrapper = function(menu) {
+      // This is useful if you want to disable some menu items before the menu is shown.
+      tree.element.trigger('xtree.openContextMenu', {
+        tree: tree,
+        node: node,
+        menu: menu
+      });
+      callback.call(tree, menu);
+    };
+
+    if (node.data.contextMenuURL) {
+      tree.contextMenuByURL = tree.contextMenuByURL || {};
+      var menu = tree.contextMenuByURL[node.data.contextMenuURL];
+      if (menu) {
+        callbackWrapper(menu);
+      } else {
+        var menuURL = node.data.contextMenuURL;
+        $.get(menuURL, function(menu) {
+          tree.contextMenuByURL[menuURL] = fixContextMenuActions(menu);
+          callbackWrapper(menu);
+        });
+      }
+    } else if (tree.contextMenuByNodeType) {
+      callbackWrapper(tree.contextMenuByNodeType[node.data.type]);
+    } else {
+      var nodeType = node.data.type;
+      $.get(tree.element.attr('data-url'), {data: 'contextMenu'}, function(contextMenuByNodeType) {
+        tree.contextMenuByNodeType = fixContextMenusActions(contextMenuByNodeType);
+        callbackWrapper(tree.contextMenuByNodeType[nodeType]);
+      });
+    }
+  };
+
+  var fixContextMenusActions = function(menus) {
+    for (var type in menus) {
+      if (menus.hasOwnProperty(type)) {
+        fixContextMenuActions(menus[type]);
+      }
+    }
+    return menus;
+  };
+
+  var fixContextMenuActions = function(menu) {
+    for (var key in menu) {
+      if (menu.hasOwnProperty(key)) {
+        var item = menu[key];
+        var actionName = item.action || key;
+        item.action = createContextMenuAction(actionName, item.parameters);
+      }
+    }
+    return menu;
+  };
+
+  var createContextMenuAction = function(action, parameters) {
+    return function (data) {
+      var tree = $.jstree.reference(data.reference);
+      // Make sure the parameters are not modified by the event listeners.
+      data.parameters = $.extend(true, {}, parameters || {});
+      tree.element.trigger('xtree.contextMenu.' + action, data);
+    }
+  }
+
+  var prepareNodeTemplate = function(parent, template) {
+    var defaultTemplate = {
+      text: 'New Child',
+      children: false,
+      data: {
+        // Make sure the specified parent can accept the new child node.
+        type: parent.data && parent.data.validChildren && parent.data.validChildren[0],
+        // Make sure the created node can be renamed and deleted.
+        canRename: true,
+        canDelete: true
+      }
+    };
+    return $.extend(true, defaultTemplate, template || {});
+  };
+
+  var getPath = function(nodeId, callback) {
+    // 'this' is the tree instance.
+    callback = $.proxy(callback, this);
+    var url = this.element.attr('data-url');
+    if (url) {
+      $.get(url, {data: 'path', 'id': nodeId})
+        .done(callback)
+        .fail(function() {
+          callback([]);
+        });
+    } else {
+      callback([]);
+    }
+  };
+
+  var openPath = function(path) {
+    // 'this' is the tree instance.
+    if (!path || !path.length) return;
+    var root = path[0];
+    if (this.get_node(root)) {
+      // The root node is present in the tree so we can open or select it.
+      if (path.length > 1) {
+        this.open_node(root, function() {
+          openPath.call(this, path.slice(1));
+        });
+      } else {
+        this.select_node(root);
+      }
+    }
+  };
+
+  var customTreeAPI = {
+    openTo: function(nodeId) {
+      if (this.get_node(nodeId)) {
+        // The specified node is already loaded in the tree.
+        this.select_node(nodeId);
+      } else {
+        // We need to load all the ancestors of the specified node.
+        getPath.call(this, nodeId, openPath);
+      }
+    },
+    execute: function(action, node, params) {
+      var url = node.data && node.data[action + 'URL'];
+      var params = params || {};
+      if (!url) {
+        url = this.element.attr('data-url');
+        params.action = action;
+        params.id = node.id;
+      }
+      params.form_token = formToken;
+      var promise = this.jobRunner.run(url, params);
+      this.element.trigger('xtree.runJob', promise);
+      return promise;
+    }
+  };
+
+  $.fn.xtree = function(params) {
+    params = $.extend(true, {
+      core: {
+        data: getChildren,
+        check_callback: validateOperation
+      },
+      plugins: ['dnd', 'contextmenu'],
+      dnd: {
+        is_draggable: areDraggable
+      },
+      contextmenu: {
+        items: getContextMenuItems
+      }
+    }, params || {});
+
+    return this.jstree(params).on('select_node.jstree', function(event, data) {
+      var tree = data.instance;
+      var selectedNode = data.node;
+      selectedNode.data && selectedNode.data.type == 'pagination' && addMoreChildren(tree, selectedNode);
+
+    //
+    // Catch events triggered when the tree structure is modified.
+    //
+
+    }).on('create_node.jstree', function(event, data) {
+      // We don't create the node right now because we want the user to specify the node name. The node will be created
+      // when the user 'renames' the node that has been created with the default name.
+
+    }).on('rename_node.jstree', function(event, data) {
+      var entityId = data.node.data && data.node.data.id;
+      if (entityId) {
+        // Rename a node that has a corresponding entity.
+        if (data.old != data.text) {
+          disableNodeBeforeLoading(data.instance, data.node);
+          moveEntity(data.instance, data.node).always(function() {
+            data.instance.refresh_node(data.node.parent);
+          });
+        }
+      } else {
+        // Create a new entity.
+        disableNodeBeforeLoading(data.instance, data.node);
+        createEntity(data.instance, data.node)
+          .done(function() {
+            data.instance.refresh_node(data.node.parent);
+          })
+          .fail(function() {
+            data.instance.delete_node(data.node);
+          });
+      }
+
+    }).on('delete_node.jstree', function(event, data) {
+      // Make sure the deleted tree node has an associated entity.
+      var entityId = data.node.data && data.node.data.id;
+      entityId && deleteEntity(data.instance, data.node).fail(function() {
+        data.instance.refresh_node(data.parent);
+      });
+
+    }).on('move_node.jstree', function(event, data) {
+      var entityId = data.node.data && data.node.data.id;
+      // Don't trigger the server-side move unless the tree node has an entity associated.
+      if (!entityId || data.parent == data.old_parent) {
+        return;
+      }
+      disableNodeBeforeLoading(data.instance, data.node);
+      moveEntity(data.instance, data.node)
+        .done(function() {
+          data.instance.refresh_node(data.parent);
+        })
+        .fail(function(response) {
+          // Undo the move.
+          // Disconnect the node from the associated entity to prevent moving the entity.
+          data.node.data.id = null;
+          data.instance.move_node(data.node, data.old_parent, data.old_position);
+          // Reconnect the tree node to the entity as soon as possible.
+          setTimeout(function() {
+            data.node.data.id = entityId;
+            enableNodeAfterLoading(data.instance, data.node);
+          }, 0);
+        });
+
+    }).on('copy_node.jstree', function(event, data) {
+      var entityId = data.original.data && data.original.data.id;
+      // Don't trigger the server-side copy unless the tree node has an entity associated.
+      if (!entityId) {
+        return;
+      }
+      disableNodeBeforeLoading(data.instance, data.node);
+      // Copy the original node meta data, without the id, to be able to undo the copy in case of failure.
+      data.node.data = $.extend(true, {}, data.original.data);
+      delete data.node.data.id;
+      copyEntity(data.instance, data.original, data.parent)
+        .done(function() {
+          data.instance.refresh_node(data.parent);
+        })
+        .fail(function(response) {
+          // Undo the copy.
+          data.instance.delete_node(data.node);
+        });
+
+    //
+    // Catch events triggered by the context menu.
+    //
+
+    }).on('xtree.contextMenu.refresh', function(event, data) {
+      var tree = $.jstree.reference(data.reference);
+      var node = tree.get_node(data.reference);
+      tree.refresh_node(node);
+
+    }).on('xtree.contextMenu.create', function(event, data) {
+      var tree = $.jstree.reference(data.reference);
+      var parent = tree.get_node(data.reference);
+      var template = prepareNodeTemplate(parent, data.parameters.template);
+      tree.create_node(parent, template, 'first', function(newNode) {
+        setTimeout(function() {
+          tree.edit(newNode);
+        }, 0);
+      });
+
+    }).on('xtree.contextMenu.cut', function(event, data) {
+      var tree = $.jstree.reference(data.reference);
+      tree.cut(tree.get_selected());
+
+    }).on('xtree.contextMenu.copy', function(event, data) {
+      var tree = $.jstree.reference(data.reference);
+      tree.copy(tree.get_selected());
+
+    }).on('xtree.contextMenu.paste', function(event, data) {
+      var tree = $.jstree.reference(data.reference);
+      var node = tree.get_node(data.reference);
+      tree.paste(node);
+
+    }).on('xtree.contextMenu.rename', function(event, data) {
+      var tree = $.jstree.reference(data.reference);
+      var node = tree.get_node(data.reference);
+      setTimeout(function() {tree.edit(node);}, 0);
+
+    }).on('xtree.contextMenu.remove', function(event, data) {
+      var skipConfirmation = data.parameters.confirmationMessage === false;
+      var confirmationMessage = data.parameters.confirmationMessage
+        || 'Are you sure you want to delete the selected nodes?';
+      // Display the confirmation after the context menu closes.
+      setTimeout(function() {
+        if (skipConfirmation || window.confirm(confirmationMessage)) {
+          var tree = $.jstree.reference(data.reference);
+          tree.delete_node(tree.get_selected());
+        }
+      }, 0);
+
+    }).on('xtree.contextMenu.openLink', function(event, data) {
+      var tree = $.jstree.reference(data.reference);
+      var node = tree.get_node(data.reference, true);
+      window.location = node.children('a.jstree-anchor').prop('href');
+
+    }).on('xtree.contextMenu.openLinkInNewTab', function(event, data) {
+      var tree = $.jstree.reference(data.reference);
+      var node = tree.get_node(data.reference, true);
+      window.open(node.children('a.jstree-anchor').prop('href'));
+
+    //
+    // Enable/Disable context menu items before the context menu is shown.
+    //
+
+    }).on('xtree.openContextMenu', function(event, data) {
+      var selectedNodes = data.tree.get_selected(true);
+      if (data.menu.copy) {
+        data.menu.copy._disabled = !canCopyNodes(selectedNodes);
+      }
+      if (data.menu.cut) {
+        data.menu.cut._disabled = !canCutNodes(selectedNodes);
+      }
+      if (data.menu.paste) {
+        data.menu.paste._disabled = !data.tree.can_paste();
+      }
+      if (data.menu.rename) {
+        data.menu.rename._disabled = !data.node.data || !data.node.data.canRename;
+      }
+      if (data.menu.remove) {
+        data.menu.remove._disabled = !canRemoveNodes(selectedNodes);
+      }
+
+    //
+    // Extend the API of the tree.
+    //
+
+    }).each(function() {
+      $.extend($.jstree.reference(this), customTreeAPI, {jobRunner: createJobRunner(this)});
+    });
+  };
+});