From 21d42343e6efd529dcd1874efbfeca05f2da0afc Mon Sep 17 00:00:00 2001
From: Mingsong Hu <mingsonghu@Mingsongs-MBP.lan>
Date: Mon, 9 Sep 2019 22:50:40 +1000
Subject: [PATCH] Replace Fancytree with jsTree

---
 hierarchy_manager.libraries.yml               |  43 +++----
 hierarchy_manager.module                      |  23 ++--
 js/Plugin/fancytree/hm.fancytree.js           |  43 +++----
 js/Plugin/jstree/hm.jstree.js                 | 105 +++++++++++++++++
 src/Controller/HmTaxonomyController.php       | 111 +++++++-----------
 src/Form/HmOverviewTerms.php                  |   2 +-
 .../HmDisplayPlugin/HmDisplayFancytree.php    |  26 ++--
 .../HmDisplayPlugin/HmDisplayJstree.php       | 100 ++++++++++++++++
 src/Plugin/HmDisplayPluginInterface.php       |  12 +-
 9 files changed, 320 insertions(+), 145 deletions(-)
 create mode 100644 js/Plugin/jstree/hm.jstree.js
 create mode 100644 src/Plugin/HmDisplayPlugin/HmDisplayJstree.php

diff --git a/hierarchy_manager.libraries.yml b/hierarchy_manager.libraries.yml
index 287c112..fae9dc7 100644
--- a/hierarchy_manager.libraries.yml
+++ b/hierarchy_manager.libraries.yml
@@ -1,49 +1,50 @@
 # Feature libraries.
 
-feature.hm.fancytree:
+feature.hm.jstree:
   js:
-    js/Plugin/fancytree/hm.fancytree.js: {}
+    js/Plugin/jstree/hm.jstree.js: {}
   dependencies:
-    - hierarchy_manager/libraries.jquery.fancytree
+    - hierarchy_manager/libraries.jquery.jstree
+    - core/drupalSettings
 
 # External libraries.
 
-libraries.jquery.fancytree:
-  remote: https://github.com/mar10/fancytree
-  version: 'v2.31.0'
+libraries.jquery.jstree:
+  remote: https://github.com/vakata/jstree
+  version: '3.3.8'
   license:
     name: MIT
-    url: https://github.com/mar10/fancytree/blob/master/LICENSE.txt
+    url: https://github.com/vakata/jstree/blob/master/LICENSE-MIT
     gpl-compatible: true
   cdn:
-    https://unpkg.com/jquery.fancytree@2.31.0/dist/
+    https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.8/
   js:
-    /libraries/jquery.fancytree/jquery.fancytree-all-deps.min.js: {minified: true}
+    /libraries/jquery.jstree/3.3.8/jstree.min.js: {minified: true}
   dependencies:
     - core/jquery
     
-libraries.jquery.fancytree.skin-win8:
-  remote: https://github.com/mar10/fancytree
-  version: 'v2.31.0'
+libraries.jquery.jstree.default:
+  remote: https://github.com/vakata/jstree
+  version: '3.3.8'
   license:
     name: MIT
-    url: https://github.com/mar10/fancytree/blob/master/LICENSE.txt
+    url: https://github.com/vakata/jstree/blob/master/LICENSE-MIT
     gpl-compatible: true
   cdn:
-    https://unpkg.com/jquery.fancytree@2.31.0/dist/skin-win8/
+    https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.8/themes/default/
   css:
     component:
-      /libraries/jquery.fancytree/skin-win8/ui.fancytree.min.css: {}
+      /libraries/jquery.jstree/3.3.8/themes/default/style.min.css: {}
     
-libraries.jquery.fancytree.skin-bootstrap:
-  remote: https://github.com/mar10/fancytree
-  version: 'v2.31.0'
+libraries.jquery.jstree.default-dark:
+  remote: https://github.com/vakata/jstree
+  version: '3.3.8'
   license:
     name: MIT
-    url: https://github.com/mar10/fancytree/blob/master/LICENSE.txt
+    url: https://github.com/vakata/jstree/blob/master/LICENSE-MIT
     gpl-compatible: true
   cdn:
-    https://unpkg.com/jquery.fancytree@2.31.0/dist/skin-bootstrap/
+    https://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.8/themes/default-dark/
   css:
     component:
-      /libraries/jquery.fancytree/skin-bootstrap/ui.fancytree.min.css: {}
\ No newline at end of file
+      /libraries/jquery.jstree/3.3.8/themes/default-dark/style.min.css: {}
\ No newline at end of file
diff --git a/hierarchy_manager.module b/hierarchy_manager.module
index 477aec4..0f5bcb8 100644
--- a/hierarchy_manager.module
+++ b/hierarchy_manager.module
@@ -11,25 +11,20 @@
 function hierarchy_manager_library_info_alter(array &$libraries, $module) {
   if ('hierarchy_manager' == $module) {
     // Use CDN instead of all local missing libraries.
-    // Fancytree min js.
-    $cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jquery.fancytree', 'js');
+    // jsTree min js.
+    $cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jquery.jstree', 'js');
     if ($cdn_library) {
-      $libraries['libraries.jquery.fancytree']['js'] = $cdn_library;
+      $libraries['libraries.jquery.jstree']['js'] = $cdn_library;
     }
-    // Fancytree drag and drop for html 5 js.
-    $cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jquery.fancytree.dnd5', 'js');
+    // jsTree default theme.
+    $cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jquery.jstree.default', 'css');
     if ($cdn_library) {
-      $libraries['libraries.jquery.fancytree.dnd5']['js'] = $cdn_library;
+      $libraries['libraries.jquery.jstree.default']['css']['component'] = $cdn_library;
     }
-    // Fancytree win-8 theme.
-    $cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jquery.fancytree.skin-win8', 'css');
+    // jsTree dark theme.
+    $cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jquery.jstree.default-dark', 'css');
     if ($cdn_library) {
-      $libraries['libraries.jquery.fancytree.skin-win8']['css']['component'] = $cdn_library;
-    }
-    // Fancytree win-8 theme.
-    $cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jquery.fancytree.skin-bootstrap', 'css');
-    if ($cdn_library) {
-      $libraries['libraries.jquery.fancytree.skin-bootstrap']['css']['component'] = $cdn_library;
+      $libraries['libraries.jquery.jstree.default-dark']['css']['component'] = $cdn_library;
     }
   }
 }
diff --git a/js/Plugin/fancytree/hm.fancytree.js b/js/Plugin/fancytree/hm.fancytree.js
index 7400be1..e44ba1f 100644
--- a/js/Plugin/fancytree/hm.fancytree.js
+++ b/js/Plugin/fancytree/hm.fancytree.js
@@ -7,7 +7,9 @@
       $treeElement.fancytree({
         extensions: ["dnd5", "filter"],
         source: {
-          url: sourceURL
+          url: sourceURL,
+          data: { depth: 1, parent: 0 },
+          cache: false
         },
         // Event handler
         dblclick: function(event, data) {
@@ -25,7 +27,8 @@
           // Load child nodes via Ajax GET sourceURL?depth=1&parent={node.key}
           data.result = {
             url: sourceURL,
-            data: { depth: 1, parent: node.key }
+            data: { depth: 1, parent: node.key },
+            cache: false
           };
         },
         filter: {
@@ -59,29 +62,13 @@
              * data.dataTransfer.setData() and .setDragImage() is available
              * here.
              */
-            // Set the allowed effects (i.e. override the 'effectAllowed' option)
-            data.effectAllowed = "all";
-
-            // Set a drop effect (i.e. override the 'dropEffectDefault' option)
-            // data.dropEffect = "link";
-            data.dropEffect = "copy";
-
-            // We could use a custom image here:
-            // data.dataTransfer.setDragImage($("<div>TEST</div>").appendTo("body")[0], -10, -10);
-            // data.useDefaultImage = false;
-
+            
             // Return true to allow the drag operation
             return true;
           },
           dragEnter: function(node, data) {
-            // data.dropEffect = "copy";
             return true;
           },
-          dragOver: function(node, data) {
-            // Assume typical mapping for modifier keys
-            data.dropEffect = data.dropEffectSuggested;
-            // data.dropEffect = "move";
-          },
           dragDrop: function(node, data) {
             /* This function MUST be defined to enable dropping of items on
              * the tree.
@@ -120,8 +107,19 @@
                   .done(response => {
                     if (response.result === "success") {
                       // Move the nodes.
-                      data.otherNode.moveTo(node, hitMode, affectedNodes => {
-                        affectedNodes.parent.folder = true;
+                      data.otherNode.moveTo(node, hitMode, function (affectedNodes) {
+                        let parentNode = affectedNodes.parent;
+                        if (parentNode) {
+                          if (!parentNode.folder) {
+                            parentNode.folder = true;
+                          } 
+                          else {
+                          /*  if (!parentNode.lazy) {
+                              parentNode.lazy = true;
+                            }
+                            parentNode.load(true);*/
+                          }        
+                        }        
                       });
                     } else {
                       alert("Server error:" + response.result);
@@ -144,9 +142,6 @@
             node.setExpanded();
           }
         },
-        activate: function(event, data) {
-          //        alert("activate " + data.node);
-        }
       });
     });
 
diff --git a/js/Plugin/jstree/hm.jstree.js b/js/Plugin/jstree/hm.jstree.js
new file mode 100644
index 0000000..69ad563
--- /dev/null
+++ b/js/Plugin/jstree/hm.jstree.js
@@ -0,0 +1,105 @@
+/**
+ * @file
+ * Hierarchy Manager jsTree JavaScript file.
+ */
+
+// Codes run both on normal page loads and when data is loaded by AJAX (or BigPipe!)
+// @See https://www.drupal.org/docs/8/api/javascript-api/javascript-api-overview
+(function($, Drupal) {
+  Drupal.behaviors.hmJSTree = {
+    attach: function(context, settings) {
+      $(".hm-jstree", context)
+        .once("jstreeBehavior")
+        .each(function() {
+          const treeContainer = $(this);
+          const parentID = treeContainer.attr('parent-id');
+          const searchTextID = (parentID) ? '#hm-jstree-search-' + parentID : '#hm-jstree-search';
+          const theme = treeContainer.attr("theme");
+          const dots = treeContainer.attr("dots");
+          const dataURL = treeContainer.attr('data-source') + '&parent=0';
+          const updateURL = treeContainer.attr('url-update')
+          let reload = true;
+          let rollback = false;
+          // Ajax callback to refresh the tree.
+          if (reload) {
+            // Build the tree.
+            treeContainer.jstree({
+              core: {
+                data: {
+                  url: function(node) {
+                    return node.id === '#' ?
+                        dataURL :
+                        dataURL;
+                  },
+                  data: function(node) {
+                    return node;
+                  }
+                },
+                themes: {
+                  // Todo: make configurable.
+                  dots: dots === "1",
+                  name: theme
+                },
+                'check_callback' : true,
+                "multiple": false,
+              },
+              search: {
+                show_only_matches: true
+              },
+              plugins: ["search", "dnd"]
+            });
+            
+           // Node move event.
+            treeContainer.on("move_node.jstree", function(event, data) {
+              const thisTree = data.instance;
+              const movedNode = data.node;
+              
+              if (!rollback) {            
+                // Update the data on server side.
+                $.post(updateURL, {
+                  keys: [movedNode.id],
+                  target: data.position,
+                  parent: data.parent
+                })
+                  .done(function(response) {
+                    if (response.result !== "success") {
+                      alert("Server error:" + response.result);
+                      rollback = true;
+                      thisTree.move_node(movedNode, data.old_parent, data.old_position);
+                    }
+                  })
+                  .fail(function() {
+                    alert("Error: Can't connect to the server.");
+                    rollback = true;
+                    thisTree.move_node(movedNode, data.old_parent, data.old_position);
+                  }); 
+              }
+              else {
+                rollback = false;
+              }
+            });
+            
+         // Node selected event.
+            treeContainer.on("select_node.jstree", function(event, data) {
+              var href = data.node.a_attr.href;
+              // Todo: make the target of the new window configurable.
+              window.open(href, "_self");
+            });
+
+            // Search filter box.
+            let to = false;
+            $(searchTextID).keyup(function() {
+              const searchInput = $(this);
+              if (to) {
+                clearTimeout(to);
+              }
+              to = setTimeout(function() {
+                const v = searchInput.val();
+                treeContainer.jstree(true).search(v);
+              }, 250);
+            });
+          }
+        });
+    }
+  };
+})(jQuery, Drupal);
diff --git a/src/Controller/HmTaxonomyController.php b/src/Controller/HmTaxonomyController.php
index cc6be29..f9c35d4 100644
--- a/src/Controller/HmTaxonomyController.php
+++ b/src/Controller/HmTaxonomyController.php
@@ -87,7 +87,11 @@ class HmTaxonomyController extends ControllerBase {
       return new Response($this->t('Access denied!'));
     }
     $parent = $request->get('parent') ?: 0;
-    $depth = $request->get('depth') ?: 1;
+    $depth = $request->get('depth');
+    
+    if(!empty($depth)) {
+      $depth = intval($depth);
+    }
 
     $vocabulary_hierarchy = $this->storageController->getVocabularyHierarchyType($vid);
     // Taxonomy tree must not be multiple parent tree.
@@ -100,16 +104,11 @@ class HmTaxonomyController extends ControllerBase {
         if ($term instanceof Term) {
           // User can only access the terms that they can update.
           if ($access_control_handler->access($term, 'update')) {
-            // Find children of this term.
-            $query = \Drupal::entityQuery('taxonomy_term')
-              ->condition('parent', $term
-                ->id());
 
-            $has_children = empty($query->execute()) ? FALSE : TRUE;
             $term_array[] = [
               'id' => $term->id(),
-              'title' => $term->label(),
-              'has_children' => $has_children,
+              'text' => $term->label(),
+              'parent' => $term->parents[0],
               'edit_url' => $term->toUrl('edit-form')->toString(),
             ];
           }
@@ -153,82 +152,52 @@ class HmTaxonomyController extends ControllerBase {
       return new Response($this->t('Access denied!'));
     }
 
-    $target_id = $request->get('target');
+    $target_position = $request->get('target');
     $parent_id = intval($request->get('parent'));
-    $mode = $request->get('mode');
     $updated_terms = $request->get('keys');
     $success = FALSE;
 
-    if (is_array($updated_terms) && !empty($updated_terms) && !empty($target_id)) {
+    if (is_array($updated_terms) && !empty($updated_terms)) {
       // Taxonomy access control.
       $access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term');
-      /*
-       * Mode firstChild: Insert terms as first children of the target term.
-       * Mode before: Insert terms before the target term as siblings.
-       * Mode after: Insert terms after the target term as siblings.
-       */
-      if ($mode === 'firstChild') {
-        // The target is the parent.
-        $parent_id = intval($target_id);
-        // All children of the parent term.
-        $children = $this->storageController->loadTree($vid, $parent_id, 1);
-        // Figure out the weight of the first child.
-        if (is_array($children) && !empty($children)) {
-          $child = reset($children);
-          // Make sure the weight is less than the first child,
-          // so that the terms will be inserted before the first child.
-          $weight = $child->weight - count($updated_terms);
-        }
+
+      // Children of the parent term in weight and name alphabetically order.
+      $children = $this->storageController->loadTree($vid, $parent_id, 1);
+      if (empty($children)) {
+        return new JsonResponse(['result' => 'fail']);
       }
-      // Insert before or after the target.
-      else {
-        $step = 0;
-        // Children of the parent term in weight and name alphabetically order.
-        $children = $this->storageController->loadTree($vid, $parent_id, 1, TRUE);
-        // Loop the children array to move other terms after the target term,
-        // include the target term if the mode is 'before'.
-        foreach ($children as $child) {
-          // Identify the target term.
-          if (($step === 0) && ((string) $child->id() === $target_id)) {
-            if ($mode === 'before') {
-              // Updated terms will be insert into the positoin of target term.
-              $weight = $child->getWeight();
-              $step = count($updated_terms) + $weight;
-            }
-            else {
-              // Updated terms will be insert after the positoin of target term.
-              $weight = $child->getWeight() + 1;
-            }
-          }
-          // Still haven't reached the target term,
-          // move the point forward.
-          elseif (!isset($weight)) {
-            continue;
-          }
-          // Start moving the terms after this point,
-          // if the step has been set.
-          if ($step) {
-            if ($child->getWeight() <= $step) {
-              $child->setWeight($step++);
-              $child->save();
-            }
-            else {
-              // The rest of children don't need to move,
-              // as their weight is greater then the gap.
-              break;
-            }
+      
+      $target_position = intval($target_position);
+      $total = count($children);
+      // Move all terms after the target position forward.
+      if (isset($children[$target_position])) {
+        $weight = (int) $children[$target_position]->weight;
+        $tids = [];
+        $step = $weight + count($updated_terms);
+        for ($i = $target_position; $i < $total; $i++) {
+          if ($children[$i]->weight < $step++) {
+            $tids[] = $children[$i]->tid;
           }
           else {
-            $step = count($updated_terms) + $weight;
+            // There is planty room, no need to move anymore.
+            break;
           }
         }
+        $step = $weight + count($updated_terms);
+        $term_siblings = Term::loadMultiple($tids);
+        foreach ($term_siblings as $term) {
+          $term->setWeight($step++);
+          $success = $term->save();
+        }
       }
-
-      if (!isset($weight)) {
-        // Set the weight to 0 as default.
-        $weight = 0;
+      elseif ($target_position === $total) {
+        // Insert into the end.
+        $weight = intval(array_slice($children, -1)[0]->weight) + 1;
       }
-
+      else {
+        return new JsonResponse(['result' => 'The term is not found.']);
+      }
+      // Load all terms needed to update.
       $terms = Term::loadMultiple($updated_terms);
       // Update all terms, the weight will be increased by 1,
       // after inserting.
diff --git a/src/Form/HmOverviewTerms.php b/src/Form/HmOverviewTerms.php
index 0ff7b3a..f4f632c 100644
--- a/src/Form/HmOverviewTerms.php
+++ b/src/Form/HmOverviewTerms.php
@@ -61,7 +61,7 @@ class HmOverviewTerms extends OverviewTerms {
                   $source_url = $base_path . $language->getId() . '/admin/hierarchy_manager/taxonomy/json/' . $vid . '?token=' . $token;
                   $update_url = $base_path . $language->getId() . '/admin/hierarchy_manager/taxonomy/update/' . $vid . '?token=' . $token;
                 }
-                return $instance->getForm($source_url, $update_url);
+                return $instance->getForm($source_url, $update_url, $form, $form_state);
               }
             }
           }
diff --git a/src/Plugin/HmDisplayPlugin/HmDisplayFancytree.php b/src/Plugin/HmDisplayPlugin/HmDisplayFancytree.php
index 85a7c87..63ded59 100644
--- a/src/Plugin/HmDisplayPlugin/HmDisplayFancytree.php
+++ b/src/Plugin/HmDisplayPlugin/HmDisplayFancytree.php
@@ -21,8 +21,8 @@ class HmDisplayFancytree extends HmDisplayPluginBase implements HmDisplayPluginI
   /**
    * {@inheritdoc}
    */
-  public function getForm(string $url_source, string $url_update, array $form = [], FormStateInterface $form_state = NULL) {
-    if (!empty($url_source)) {
+  public function getForm(string $url_source, string $url_update, array &$form = [], FormStateInterface &$form_state = NULL) {
+  /*  if (!empty($url_source)) {
       // Search input.
       $form['title'] = [
         '#type' => 'textfield',
@@ -54,7 +54,7 @@ class HmDisplayFancytree extends HmDisplayPluginBase implements HmDisplayPluginI
       $form['#attached']['library'][] = 'hierarchy_manager/libraries.jquery.fancytree.skin-win8';
       $form['#attached']['library'][] = 'hierarchy_manager/feature.hm.fancytree';
     }
-
+*/
     return $form;
   }
 
@@ -62,19 +62,21 @@ class HmDisplayFancytree extends HmDisplayPluginBase implements HmDisplayPluginI
    * Build the data array that FancyTree accepts.
    */
   public function treeData(array $data) {
-    $tree_data = [];
+    $fancytree_data = [];
 
+    // The array key of Fancytree is different from the data source.
+    // So we need to translate them.
     foreach ($data as $tree_node) {
-      $tree_data[] = [
-        'title' => $tree_node['title'],
-        'folder' => $tree_node['has_children'],
-        'key' => $tree_node['id'],
-        'lazy' => $tree_node['has_children'],
-        'edit_url' => $tree_node['edit_url'],
-      ];
+      $fancytree_node = $tree_node;
+      $fancytree_node['key'] = $fancytree_node['id'];
+      unset($fancytree_node['id']);
+      $fancytree_node['folder'] = $fancytree_node['lazy'] = $fancytree_node['has_children'];
+      unset($fancytree_node['has_children']);
+      // Add this node into the data array.
+      $fancytree_data[] = $fancytree_node;
     }
 
-    return $tree_data;
+    return $fancytree_data;
   }
 
 }
diff --git a/src/Plugin/HmDisplayPlugin/HmDisplayJstree.php b/src/Plugin/HmDisplayPlugin/HmDisplayJstree.php
new file mode 100644
index 0000000..107d44b
--- /dev/null
+++ b/src/Plugin/HmDisplayPlugin/HmDisplayJstree.php
@@ -0,0 +1,100 @@
+<?php
+
+namespace Drupal\hierarchy_manager\Plugin\HmDisplayPlugin;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\hierarchy_manager\Plugin\HmDisplayPluginInterface;
+use Drupal\hierarchy_manager\Plugin\HmDisplayPluginBase;
+
+/**
+ * JsTree display plugin.
+ *
+ * @HmDisplayPlugin(
+ *   id = "hm_display_jstree",
+ *   label = @Translation("JsTree")
+ * )
+ */
+class HmDisplayJstree extends HmDisplayPluginBase implements HmDisplayPluginInterface {
+  use StringTranslationTrait;
+  
+  /*
+   * Build the tree form.
+   */
+  public function getForm(string $url_source, string $url_update, array &$form = [], FormStateInterface &$form_state = NULL, array $options = []) {
+    if (!empty($url_source)) {
+      if (!empty(($form_state))) {
+        $parent_formObj = $form_state->getFormObject();
+        $parent_id = $parent_formObj->getFormId();
+      }
+      
+      // The jsTree theme.
+      $theme = isset($options['theme']) ? $options['theme'] : 'default';
+     
+      // Search input.
+      $form['search'] = [
+        '#type' => 'textfield',
+        '#title' => $this
+        ->t('Search'),
+        '#description' => $this->t('Type in the search keyword here to filter the tree below. Empty the keyword to reset the tree.'),
+        '#attributes' => [
+          'name' => 'jstree-search',
+          'id' => isset($parent_id) ? 'hm-jstree-search-' . $parent_id : 'hm-jstree-search',
+          'parent-id' => isset($parent_id) ? $parent_id : '',
+          'class' => [
+            'hm-jstree-search',
+          ],
+        ],
+        '#size' => 60,
+        '#maxlength' => 128,
+      ];
+      
+      $form['jstree'] = [
+        '#type' => 'html_tag',
+        '#suffix' => '<div class="description">' . $this->t('You can double click a tree node to edit it.') . '<br>' . $this->t('The tree node is draggable and droppable') . '</div>',
+        '#tag' => 'div',
+        '#value' => '',
+        '#attributes' => [
+          'class' => [
+            'hm-jstree',
+          ],
+          'id' => isset($parent_id) ? 'hm-jstree-' . $parent_id : 'hm-jstree',
+          'parent-id' => isset($parent_id) ? $parent_id : '',
+          'theme' => $theme,
+          'data-source' => $url_source,
+          'url-update' => $url_update,
+        ],
+      ];
+      
+      $form['#attached']['library'][] = 'hierarchy_manager/libraries.jquery.jstree.' . $theme;
+      $form['#attached']['library'][] = 'hierarchy_manager/feature.hm.jstree';
+    }
+    
+    return $form;
+    
+  }
+  
+  /**
+   * Build the data array that JS library accepts.
+   */
+  public function treeData(array $data) {
+    $jstree_data = [];
+    
+    // The array key of jsTree is different from the data source.
+    // So we need to translate them.
+    foreach ($data as $tree_node) {
+      $jstree_node = $tree_node;
+      // The root id for jsTree is #.
+      if ($tree_node['parent'] === '0') {
+        $jstree_node['parent'] = '#';
+      }
+      // Custom data
+      $jstree_node['a_attr'] = ['href' => $jstree_node['edit_url']];
+      unset($jstree_node['edit_url']);
+      // Add this node into the data array.
+      $jstree_data[] = $jstree_node;
+    }
+    
+    return $jstree_data;
+  }
+}
diff --git a/src/Plugin/HmDisplayPluginInterface.php b/src/Plugin/HmDisplayPluginInterface.php
index 6b0b0ed..e023b5c 100644
--- a/src/Plugin/HmDisplayPluginInterface.php
+++ b/src/Plugin/HmDisplayPluginInterface.php
@@ -3,12 +3,20 @@
 namespace Drupal\hierarchy_manager\Plugin;
 
 use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Form\FormStateInterface;
 
 /**
  * Defines an interface for Hierarchy manager display plugin plugins.
  */
 interface HmDisplayPluginInterface extends PluginInspectionInterface {
 
-
-  // Add get/set methods for your plugin type here.
+  /*
+   * Build the tree form.
+   */
+  public function getForm(string $url_source, string $url_update, array &$form = [], FormStateInterface &$form_state = NULL, array $options = []);
+  
+  /**
+   * Build the data array that JS library accepts.
+   */
+  public function treeData(array $data);
 }
-- 
GitLab