Skip to content
Snippets Groups Projects
Commit 21d42343 authored by Mingsong Hu's avatar Mingsong Hu
Browse files

Replace Fancytree with jsTree

parent 0b2c242b
No related branches found
No related tags found
No related merge requests found
# 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
......@@ -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;
}
}
}
......
......@@ -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);
}
});
});
......
/**
* @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);
......@@ -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.
......
......@@ -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);
}
}
}
......
......@@ -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;
}
}
<?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;
}
}
......@@ -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);
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment