diff --git a/hierarchy_manager.libraries.yml b/hierarchy_manager.libraries.yml index 287c112d230a720443ae2b819a6fc7e576196054..fae9dc777e0759605a7a859de5b99a7c55a38d15 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 477aec4519886783a85cc14af3a6b64a30bfb843..0f5bcb8c94ffd6d95c4c1d1c54a93bf7c549c2a9 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 7400be1da670d69bb8b576465d05ce8055d15477..e44ba1fc6ae71da4d9dcb8437e4b733658ff486f 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 0000000000000000000000000000000000000000..69ad563898868e09648104f0f43794a865309d47 --- /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 cc6be29a2a981b79946fa5f5826935ffba37db6e..f9c35d40a5968762e036fe523b4baa37479d677d 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 0ff7b3a6fa6cb28bbb91e809470b1620881dc712..f4f632c963128e9da44b1faa3dcbd6386f225ee4 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 85a7c8769b73b746c854c3221eb86ef9424dcbb5..63ded5924905e5f7ea0ff0ea2af14b0be779009b 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 0000000000000000000000000000000000000000..107d44b6b321467a36dd81956d86ee9f30e8d2e5 --- /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 6b0b0ed14a8d9b9fec06e366c28514ee87eafe7d..e023b5cd49145a38714565833bfd39e566fdbfd8 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); }