Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • project/hierarchy_manager
  • issue/hierarchy_manager-3188871
  • issue/hierarchy_manager-3188833
  • issue/hierarchy_manager-3191599
  • issue/hierarchy_manager-3191605
  • issue/hierarchy_manager-3205538
  • issue/hierarchy_manager-3217994
  • issue/hierarchy_manager-3230813
  • issue/hierarchy_manager-3241543
  • issue/hierarchy_manager-3243559
  • issue/hierarchy_manager-3243579
  • issue/hierarchy_manager-3278219
  • issue/hierarchy_manager-3341369
  • issue/hierarchy_manager-3344493
  • issue/hierarchy_manager-3343978
  • issue/hierarchy_manager-3347488
  • issue/hierarchy_manager-3347499
  • issue/hierarchy_manager-3343297
  • issue/hierarchy_manager-3451974
  • issue/hierarchy_manager-3467198
20 results
Show changes
Commits on Source (62)
Showing
with 1221 additions and 356 deletions
hmconfig
hmsetup
Jsoneditor
jsoneditor
Jstree
jstree
Mingsong
pluginable
\ No newline at end of file
################
# GitLabCI template for Drupal projects.
#
# This template is designed to give any Contrib maintainer everything they need to test, without requiring modification.
# It is also designed to keep up to date with Core Development automatically through the use of include files that can be centrally maintained.
# As long as you include the project, ref and three files below, any future updates added by the Drupal Association will be used in your
# pipelines automatically. However, you can modify this template if you have additional needs for your project.
# The full documentation is on https://project.pages.drupalcode.org/gitlab_templates/
################
# For information on alternative values for 'ref' see https://project.pages.drupalcode.org/gitlab_templates/info/templates-version/
# To test a Drupal 7 project, change the first include filename from .main.yml to .main-d7.yml
include:
- project: $_GITLAB_TEMPLATES_REPO
ref: $_GITLAB_TEMPLATES_REF
file:
- "/includes/include.drupalci.main.yml"
- "/includes/include.drupalci.variables.yml"
- "/includes/include.drupalci.workflows.yml"
################
# Pipeline configuration variables are defined with default values and descriptions in the file
# https://git.drupalcode.org/project/gitlab_templates/-/blob/main/includes/include.drupalci.variables.yml
# Uncomment the lines below if you want to override any of the variables. The following is just an example.
################
variables:
SKIP_ESLINT: '1'
SKIP_PHPSTAN: '1'
# OPT_IN_TEST_NEXT_MAJOR: '1'
# _CURL_TEMPLATES_REF: 'main'
......@@ -10,18 +10,33 @@ CONTENTS OF THIS FILE
INTRODUCTION
------------
Drupal provides a draggable table to manage the hierarchy of menu links and taxonomy terms. The Drupal draggable table is not able to present a massive hierarchy in one page.
Drupal provides a draggable table to manage the hierarchy of menu links
and taxonomy terms.
The Drupal draggable table is not able to present a massive hierarchy
in one page.
This module provides a plugin mechanism to manage hierarchy for taxonomy terms, menu links and others. There are two out of box plugins, taxonomy hierarchy management plugin and menu hierarchy plugin. The front-end JavaScript libraries is also pluginable. The out of box display plugin using Francytree to render the hierarchy tree with filter. The hierarchy tree is draggable which means you can drag and drop a taxonomy term in the tree.
This module provides a plugin architecture to delivery a flexibility
of managing hierarchy for taxonomy terms,
menu links and others. There are two out of box plugins,
taxonomy hierarchy management plugin and menu hierarchy plugin.
The front-end JavaScript libraries is also pluginable.
The out of box display plugin using jsTree to render
the hierarchy tree with filter.
The hierarchy tree is draggable which means you can update
the hierarchy by dragging a node in the tree.
Other modules can define their own management plugin to manage hierarchy for any other entities or display plugin to render the hierarchy tree by other JavaScript libraries.
Other modules can define their own management plugin to manage
hierarchy for any other entities or display plugin to render
the hierarchy tree by a JavaScript library other than jsTree.
REQUIREMENTS
------------
This module requires the following library:
* Fancytree JS (This module will automatically load this library from romte CDN if it wasn't hosted locally under /libraries/jquery.fancytree/ folder)
* jsTree JS (This module will automatically load this library
from remote CDN if it wasn't hosted locally under
/libraries/jquery.jstree/3.3.8/ folder)
INSTALLATION
------------
......@@ -31,13 +46,23 @@ INSTALLATION
CONFIGURATION
-------------
* Go the hierarchy manage display management page (/admin/structure/hm_display_profile) under the Structure menu to create a display profile
* Go the hierarchy manage display management page
(/admin/structure/hm_display_profile) under
the Structure menu to create a display profile
* Go to the hierarchy management configuration page (/admin/config/user-interface/hierarchy_manager/config) to enable hierarchy management plugins, such as taxonomy plugin, and specify a display profile created in step above.
* Go to the hierarchy management configuration page
(/admin/config/user-interface/hierarchy_manager/config) to
enable hierarchy management plugins, such as taxonomy plugin,
and specify a display profile created in step above.
* Once a hierarchy mange plugin is enabled, the related edit form should be replaced with a hierarchy tree form. For instance, the taxonomy term edit form (/admin/structure/taxonomy/manage/{tid}/overview) will be replaced with a hierarchy tree implemented by the taxonomy hierarchy manage plugin.
* Once a hierarchy mange plugin is enabled, the related edit form
should be replaced with a hierarchy tree form.
For instance, the taxonomy term edit form
(/admin/structure/taxonomy/manage/{tid}/overview)
will be replaced with a hierarchy tree implemented by
the taxonomy hierarchy manage plugin.
MAINTAINERS
-----------
Mingsong Hu (Mingsong) - https://www.drupal.org/u/mingsong
\ No newline at end of file
Mingsong Hu (Mingsong) - https://www.drupal.org/u/mingsong
hierarchy_manager:
hierarchy_manager.hmconfig:
type: config_object
label: 'Hierarchy Manager Configuration'
mapping:
allowed_setup_plugins:
type: sequence
label: 'Allowed Setup Plugins'
sequence:
type: string
label: 'Setup Plugin ID'
setup_plugin_settings:
label: 'TFA validation plugin configuration'
type: sequence
sequence:
type: hierarchy_manager.HmSetupPlugin.plugin.config.[%key]
hierarchy_manager.HmSetupPlugin.plugin.config.hm_setup_taxonomy:
type: mapping
label: 'HM Setup Taxonomy Settings'
mapping:
display_profile:
type: string
label: 'Display Profile'
bundle:
type: sequence
label: 'Bundle'
sequence:
type: string
label: 'Bundle Name'
hierarchy_manager.HmSetupPlugin.plugin.config.hm_setup_menu:
type: mapping
label: 'HM Setup Menu Settings'
mapping:
display_profile:
type: string
label: 'Display Profile'
bundle:
type: sequence
label: 'Bundle'
sequence:
type: string
label: 'Bundle Item'
hierarchy_manager.hm_display_profile.*:
type: config_entity
label: 'Hierarchy Manager Display Profile'
mapping:
id:
type: string
label: 'ID'
label:
type: string
label: 'Label'
plugin:
type: string
label: 'Plugin'
config:
type: string
label: 'Config'
confirm:
type: boolean
label: 'Confirm'
hierarchy_manager.hm_display_profile.*:
type: config_entity
label: 'HM Display Profile Entity config'
mapping:
id:
type: string
label: 'ID'
label:
type: label
label: 'Label'
uuid:
type: string
plugin:
type: string
label: 'Display plugin'
/* HM Tree item status (unpublished/disabled) */
.jstree-default .jstree-node.hm-tree-node-disabled > a {
color: gray;
background-color: #fff4f4;
}
/* HM menu labeling */
.hm-tree-label {
font-style: italic;
}
name: Hierarchy Manager
description: Provides API and plugins to build hierarchy views for entites such as taxonomy or menu.
description: Provides API and plugins to build hierarchy views for entities such as taxonomy or menu.
package: Administration
type: module
core: 8.x
\ No newline at end of file
core_version_requirement: ^9.2.0 || ^10 || ^11
configure: hierarchy_manager.hm_config_form
<?php
/**
* @file
* Install, update and uninstall functions for the Hierarchy Manager module.
*/
use Drupal\system\Entity\Menu;
/**
* Enable hierarchy manager for all bundles of active setup plugins.
*/
function hierarchy_manager_update_8001() {
if ($config = \Drupal::configFactory()->getEditable('hierarchy_manager.hmconfig')) {
if ($allowed_setup_plugins = $config->get('allowed_setup_plugins')) {
if (!empty($allowed_setup_plugins['hm_setup_menu'])) {
$menus = Menu::loadMultiple();
$bundles = [];
/** @var \Drupal\system\Entity\Menu $menu */
foreach ($menus as $menu) {
$id = $menu->id();
$bundles[$id] = $id;
}
$config->set('setup_plugin_settings.hm_setup_menu.bundle', $bundles);
$config->save();
}
if (!empty($allowed_setup_plugins['hm_setup_taxonomy'])) {
$vocabularies = \Drupal::service('entity_type.bundle.info')->getBundleInfo('taxonomy_term');
$bundles = [];
foreach ($vocabularies as $key => $value) {
$bundles[$key] = $key;
}
$config->set('setup_plugin_settings.hm_setup_taxonomy.bundle', $bundles);
$config->save();
}
drupal_flush_all_caches();
}
}
}
# Feature libraries.
feature.hm.fancytree:
feature.hm.jstree:
js:
js/Plugin/fancytree/hm.fancytree.js: {}
js/Plugin/jstree/hm.jstree.js: {}
css:
theme:
css/Plugin/jstree/hm.jstree.css: {}
dependencies:
- hierarchy_manager/libraries.jquery.fancytree
- hierarchy_manager/libraries.jquery.jstree
- core/drupalSettings
- core/drupal.message
- core/once
feature.hm.jsoneditor:
js:
js/Plugin/jsoneditor/hm.jsoneditor.js: {}
dependencies:
- hierarchy_manager/libraries.jsoneditor
# 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.15'
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.15/
js:
/libraries/jquery.fancytree/jquery.fancytree-all-deps.min.js: {minified: true}
/libraries/jquery.jstree/3.3.15/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.15'
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.15/themes/default/
css:
component:
/libraries/jquery.fancytree/skin-win8/ui.fancytree.min.css: {}
libraries.jquery.fancytree.skin-bootstrap:
remote: https://github.com/mar10/fancytree
version: 'v2.31.0'
/libraries/jquery.jstree/3.3.15/themes/default/style.min.css: {}
libraries.jquery.jstree.default-dark:
remote: https://github.com/vakata/jstree
version: '3.3.15'
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://cdnjs.cloudflare.com/ajax/libs/jstree/3.3.15/themes/default-dark/
css:
component:
/libraries/jquery.jstree/3.3.15/themes/default-dark/style.min.css: {}
libraries.jsoneditor:
remote: https://github.com/josdejong/jsoneditor
version: '9.9.2'
license:
name: Apache License 2.0
url: https://github.com/josdejong/jsoneditor/blob/develop/LICENSE
gpl-compatible: true
cdn:
https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/9.9.2/
js:
/libraries/jsoneditor/9.9.2/jsoneditor.min.js: {minified: true}
libraries.jsoneditor.default-theme:
remote: https://github.com/josdejong/jsoneditor
version: '9.9.2'
license:
name: Apache License 2.0
url: https://github.com/josdejong/jsoneditor/blob/develop/LICENSE
gpl-compatible: true
cdn:
https://unpkg.com/jquery.fancytree@2.31.0/dist/skin-bootstrap/
https://cdnjs.cloudflare.com/ajax/libs/jsoneditor/9.9.2/
css:
component:
/libraries/jquery.fancytree/skin-bootstrap/ui.fancytree.min.css: {}
\ No newline at end of file
/libraries/jsoneditor/9.9.2/jsoneditor.min.css: {minified: true}
......@@ -7,8 +7,8 @@ hierarchy_manager.hm_config_form:
# HM Display Profile Entity menu items definition
entity.hm_display_profile.collection:
title: 'HM Display Profile Entity'
title: 'HM Display Profile'
route_name: entity.hm_display_profile.collection
description: 'List HM Display Profile Entity (bundles)'
description: 'List HM Display Profiles'
parent: system.admin_structure
weight: 99
......@@ -5,35 +5,65 @@
* General functions and hook implementations for Hierarchy Manager module.
*/
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Implements hook_help().
*/
function hierarchy_manager_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
// Main module help for the hierarchy_manager module.
case 'help.page.hierarchy_manager':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('Hierarchy Manager module provides a flexible solution for managing hierarchies of menu links and taxonomy terms. Unlike the default Drupal draggable table, this module supports massive hierarchies and offers a plugin architecture for customization.') . '</p>';
$output .= '<p>' . t('Out of the box, the module comes with two plugins: a taxonomy hierarchy management plugin and a menu hierarchy plugin. The front-end JavaScript libraries are also pluginable, with an out-of-the-box display plugin using jsTree to render the hierarchy tree with a filter. The hierarchy tree is draggable, allowing you to easily update the hierarchy by dragging a node in the tree.') . '</p>';
return $output;
}
}
/**
* Implements hook_library_info_alter().
*/
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;
$libraries['libraries.jquery.jstree.default-dark']['css']['component'] = $cdn_library;
}
// Fancytree win-8 theme.
$cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jquery.fancytree.skin-bootstrap', 'css');
// Jsoneditor min js.
$cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jsoneditor', 'js');
if ($cdn_library) {
$libraries['libraries.jquery.fancytree.skin-bootstrap']['css']['component'] = $cdn_library;
$libraries['libraries.jsoneditor']['js'] = $cdn_library;
}
// Jsoneditor default theme.
$cdn_library = _hierarchy_manager_use_cdn($libraries, 'libraries.jsoneditor.default-theme', 'css');
if ($cdn_library) {
$libraries['libraries.jsoneditor.default-theme']['css']['component'] = $cdn_library;
}
}
}
/**
* Implements hook_entity_type_alter().
*/
function hierarchy_manager_entity_type_alter(array &$entity_types) {
// Override the menu edit form.
$entity_types['menu']
->setFormClass('edit', 'Drupal\hierarchy_manager\Form\HmMenuForm');
}
/**
* Replace local library with CDN.
*
......
# HM Display Profile list
entity.hm_display_profile.collection:
path: '/admin/structure/hm_display_profile'
defaults:
_entity_list: 'hm_display_profile'
_title: 'HM Display Profiles'
requirements:
_permission: 'administer site configuration'
# Hierarchy Manager Configuration
hierarchy_manager.hm_config_form:
path: '/admin/config/user-interface/hierarchy_manager/config'
defaults:
_form: '\Drupal\hierarchy_manager\Form\HMConfigForm'
_title: 'HMConfigForm'
_title: 'Hierarchy Manager'
requirements:
_permission: 'access administration pages'
_permission: 'administer site configuration'
options:
_admin_route: TRUE
# Taxonomy display plugin.
# Taxonomy hierarchy plugin.
hierarchy_manager.taxonomy.tree.json:
path: '/admin/hierarchy_manager/taxonomy/json/{vid}'
defaults:
_title: 'Taxonomy tree'
_controller: '\Drupal\hierarchy_manager\Controller\HmTaxonomyController::taxonomyTreeJson'
requirements:
_permission: 'administer taxonomy'
_custom_access: '\Drupal\hierarchy_manager\Controller\HmTaxonomyController::access'
options:
_admin_route: TRUE
hierarchy_manager.taxonomy.tree.update:
......@@ -25,7 +34,26 @@ hierarchy_manager.taxonomy.tree.update:
_title: 'Taxonomy tree'
_controller: '\Drupal\hierarchy_manager\Controller\HmTaxonomyController::updateTerms'
requirements:
_permission: 'administer taxonomy'
_custom_access: '\Drupal\hierarchy_manager\Controller\HmTaxonomyController::access'
options:
_admin_route: TRUE
# Menu hierarchy plugin.
hierarchy_manager.menu.tree.json:
path: '/admin/hierarchy_manager/menu/json/{mid}'
defaults:
_title: 'Menu tree'
_controller: '\Drupal\hierarchy_manager\Controller\HmMenuController::menuTreeJson'
requirements:
_permission: 'administer menu'
options:
_admin_route: TRUE
hierarchy_manager.menu.tree.update:
path: '/admin/hierarchy_manager/menu/update/{mid}'
defaults:
_title: 'Menu tree'
_controller: '\Drupal\hierarchy_manager\Controller\HmMenuController::updateMenuLinks'
requirements:
_permission: 'administer menu'
options:
_admin_route: TRUE
services:
# Plugins
plugin.manager.hm.hmsetup:
class: Drupal\hierarchy_manager\Plugin\HmSetupPluginManager
parent: default_plugin_manager
plugin.manager.hm.display_plugin:
class: Drupal\hierarchy_manager\Plugin\HmDisplayPluginManager
parent: default_plugin_manager
# Event subscriber
hm.route_subscriber:
class: Drupal\hierarchy_manager\Routing\HmRouteSubscriber
tags:
- { name: event_subscriber }
# Custom services
hm.plugin_type_manager:
class: Drupal\hierarchy_manager\PluginTypeManager
arguments: ['@entity_type.manager', '@plugin.manager.hm.display_plugin', '@plugin.manager.hm.hmsetup']
tags:
- { name: hm_plugin_type_manager, priority: 1000 }
(function($, Drupal) {
$(document).ready(() => {
$(".fancytree").each(function(index) {
const $treeElement = $(this);
const sourceURL = $treeElement.attr("data-source");
const updateURL = $treeElement.attr("url-update");
$treeElement.fancytree({
extensions: ["dnd5", "filter"],
source: {
url: sourceURL
},
// Event handler
dblclick: function(event, data) {
const node = data.node;
if (node.data.edit_url) {
event.stopPropagation();
event.preventDefault();
// Todo: make the target of the new window configurable.
window.open(node.data.edit_url, "_self");
}
},
// Called when a lazy node is expanded for the first time:
lazyLoad: function(event, data) {
const node = data.node;
// Load child nodes via Ajax GET sourceURL?depth=1&parent={node.key}
data.result = {
url: sourceURL,
data: { depth: 1, parent: node.key }
};
},
filter: {
autoApply: true, // Re-apply last filter if lazy data is loaded
autoExpand: true, // Expand all branches that contain matches while filtered
counter: true, // Show a badge with number of matching child nodes near parent icons
fuzzy: false, // Match single characters in order, e.g. 'fb' will match 'FooBar'
hideExpandedCounter: false, // Hide counter badge if parent is expanded
hideExpanders: false, // Hide expanders if all child nodes are hidden by filter
highlight: true, // Highlight matches by wrapping inside <mark> tags
leavesOnly: false, // Match end nodes only
nodata: true, // Display a 'no data' status node if result is empty
mode: "hide" // Grayout unmatched nodes (pass "hide" to remove unmatched node instead)
},
dnd5: {
// autoExpandMS: 400,
preventForeignNodes: true,
preventNonNodes: true,
preventRecursion: true, // Prevent dropping nodes on own descendants
// preventSameParent: true,
preventVoidMoves: true, // Prevent moving nodes 'before self', etc.
// effectAllowed: "all",
// dropEffectDefault: "move", // "auto",
// --- Drag-support:
dragStart: function(node, data) {
/* This function MUST be defined to enable dragging for the tree.
*
* Return false to cancel dragging of node.
* 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.
*/
const mode = data.dropEffect;
if (data.otherNode) {
// Drop another Fancytree node from same frame (maybe a different tree however)
// var sameTree = (data.otherNode.tree === data.tree);
if (mode === "move") {
let parentKey;
const childrenNodeIDs = [];
let i = 0;
const hitMode =
data.hitMode === "over" ? "firstChild" : data.hitMode;
// Get all nodes moving.
data.otherNodeList.forEach(element => {
childrenNodeIDs[i++] = element.key;
});
// The parent key of the target.
parentKey = node.parent.key;
// For drupal, the ID of the root node is 0.
if (parentKey === "root_1") {
parentKey = 0;
}
// Update the data on server side.
$.post(updateURL, {
keys: childrenNodeIDs,
target: node.key,
parent: parentKey,
mode: hitMode
})
.done(response => {
if (response.result === "success") {
// Move the nodes.
data.otherNode.moveTo(node, hitMode, affectedNodes => {
affectedNodes.parent.folder = true;
});
} else {
alert("Server error:" + response.result);
}
})
.fail(() => {
alert("Error: Can't connect to the server.");
});
} else {
/* Todo: duplicate nodes
*/
}
} else if (data.otherNodeData) {
// Drop Fancytree node from different frame or window, so we only have
// JSON representation available
/* Todo: move node from different tree
node.addChild(data.otherNodeData, data.hitMode);
*/
}
node.setExpanded();
}
},
activate: function(event, data) {
// alert("activate " + data.node);
}
});
});
// Event handlers.
$("input[name=fancytree-search]")
.on("keyup", function(e) {
// Todo: Deal with multiple tree and search text field.
const tree = $.ui.fancytree.getTree();
const match = $(this).val();
if ((e && e.which === $.ui.keyCode.ESCAPE) || $.trim(match) === "") {
tree.clearFilter();
return;
}
// Pass a string to perform case insensitive matching
tree.filterBranches(match);
})
.focus();
});
})(jQuery, Drupal);
// Codes just run once the DOM has loaded.
// @See https://www.drupal.org/docs/8/api/javascript-api/javascript-api-overview
(function($) {
const editorID = "json-editor";
const valueID = "config-value";
var container = document.getElementById(editorID);
var data = {};
var options = {
mode: "tree",
modes: ['code', 'tree'], // allowed modes
name: "Configuration",
};
// json hidden element
var jsonInput = document.getElementById(valueID);
//json data
if (jsonInput && jsonInput.value) {
data = JSON.parse(jsonInput.value);
}
var editor = new JSONEditor(container, options, data);
editor.setName('Configuration');
//The input form.
var form = jsonInput.form;
var submit = function(e) {
jsonInput.value = JSON.stringify(editor.get());
};
// Form submit event.
if(form.addEventListener){
form.addEventListener("submit", submit); //Modern browsers
}else if(ele.attachEvent){
form.attachEvent('onsubmit', submit); //Old IE
}
})(jQuery);
\ No newline at end of file
/**
* @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, once) {
Drupal.behaviors.hmJSTree = {
attach: function(context, settings) {
const hmJstree = once('hmJSTree', '.hm-jstree', context);
// Render all trees.
hmJstree.forEach(function(hmJstree) {
const treeContainer = $(hmJstree);
const parentID = treeContainer.attr('parent-id');
const searchTextID = (parentID) ? '#hm-jstree-search-' + parentID : '#hm-jstree-search';
const optionsJson = treeContainer.attr("options");
const dataURL = treeContainer.attr('data-source') + '&parent=0';
const updateURL = treeContainer.attr('url-update');
const confirm = treeContainer.attr("confirm");
let reload = true;
let rollback = false;
let themes = {
dots: false,
name: 'default'
};
let options;
if (optionsJson) {
options = JSON.parse(optionsJson);
if (options.theme) {
themes = options.theme;
}
}
// Ajax callback to refresh the tree.
if (reload) {
// Build the tree.
treeContainer.jstree({
core: {
'check_callback' : function (operation, node, node_parent, node_position, more) {
return true;
},
data: {
url: function(node) {
return node.id === '#' ?
dataURL :
dataURL;
},
data: function(node) {
return node;
}
},
themes: themes,
"multiple": false,
},
'dnd' : {
'copy': false,
'is_draggable' : function(node) {
let can_drag = node[0].data.draggable;
if (can_drag) {
return true;
}
else {
let drupalMessages = new Drupal.Message();
drupalMessages.clear();
drupalMessages.add(Drupal.t("Cannot drag this item, possibly because it has multiple parents or ancestors."), {type: 'warning'});
return false;
}
}
},
search: {
show_only_matches: true,
"search_callback": function(str, node) {
//search for any of the words entered
var word, words = [];
var searchFor = str.toLowerCase().replace(/^\s+/g, '').replace(/\s+$/g, '');
if (searchFor.indexOf(' ') >= 0) {
words = searchFor.split(' ');
} else {
words = [searchFor];
}
for (var i = 0; i < words.length; i++) {
word = words[i];
if ((node.text || "").toLowerCase().indexOf(word) >= 0) {
return true;
}
}
return false;
}
},
'sort' : function(a, b) {
return parseInt(this.get_node(a).data.weight) > parseInt(this.get_node(b).data.weight) ? 1 : -1;
},
plugins: ["search", "dnd", "sort"]
});
// Node move event.
treeContainer.on("move_node.jstree", function(event, data) {
const thisTree = data.instance;
const movedNode = data.node;
const parent = data.parent === '#' ? 0 : data.parent;
const parent_node = thisTree.get_node(data.parent);
const old_parent = data.old_parent === '#' ? 0 : data.old_parent;
const drupalMessages = new Drupal.Message();
if (!rollback) {
let parentText = Drupal.t('root');
if (parent !== 0) {
parentText = $("<div/>").html(thisTree.get_node(parent).text);
parentText.find("span").remove();
parentText = parentText.text();
}
// Function to move the tree item.
function moveTreeItem() {
// Update the data on server side.
$.post(updateURL, {
keys: [movedNode.id],
target: data.position,
parent: parent,
old_parent: old_parent,
old_position: data.old_position
})
.done(function(response) {
if (response.result !== "success") {
alert("Server error:" + response.result);
rollback = true;
thisTree.move_node(movedNode, data.old_parent, data.old_position);
}
else {
if (parent_node.data && !parent_node.data.draggable) {
// The parent node is not draggable.
// We have to update all duplicated nodes
// by refreshing the whole tree.
thisTree.refresh();
}
else {
// Update the nodes changed in the server side.
if (response.updated_nodes) {
let update_nodes = response.updated_nodes;
for (const id in update_nodes) {
let node = thisTree.get_node(id);
if (node) {
node.data.weight = update_nodes[id];
}
}
//Refresh the tree without reloading data from server.
thisTree.sort(parent_node, true);
thisTree.redraw(true);
}
}
let message = Drupal.t('@node is moved to position @position under @parent', {'@node': data.node.text, '@parent': parentText, '@position': data.position + 1});
// Inform user the movement.
drupalMessages.clear();
drupalMessages.add(message);
}
})
.fail(function() {
drupalMessages.clear();
drupalMessages.add(Drupal.t("Can't connect to the server."), {type: 'error'});
rollback = true;
thisTree.move_node(movedNode, data.old_parent, data.old_position);
});
}
// Check if confirmation dialog is enabled.
if (typeof confirm !== 'undefined' && confirm !== false) {
// Confirmation dialog enabled.
let modalTitle = Drupal.t('Confirm move?');
let modalMessage = Drupal.t('Move <em class="placeholder">@node</em> to position @position under <em class="placeholder">@parent</em>?', { '@node': data.node.text, '@parent': parentText, '@position': data.position + 1 });
modalConfirmation(modalTitle, modalMessage, moveTreeItem, function () {
// Callback when confirmation is denied.
rollback = true;
thisTree.move_node(movedNode, data.old_parent, data.old_position);
});
} else {
// Confirmation dialog disabled.
moveTreeItem()
}
}
else {
rollback = false;
}
});
treeContainer.on('ready.jstree open_node.jstree move_node.jstree search.jstree clear_search.jstree redraw.jstree', function (event, data) {
Drupal.attachBehaviors(event.target);
});
// 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);
});
}
});
}
};
/**
* Generic modal helper function.
*
* @param {string} title - The title for the confirm dialog.
* @param {string} message - The main message for the confirm dialog.
* @param {function} accept - Callback fired when the user answers positive.
* @param {function} deny - Callback fired when the user answers negative.
* @returns {Object} - A jQuery dialog object.
*/
function modalConfirmation(title, message, accept, deny) {
let proceed = false;
let modalConfirmationForm = $('<div></div>').appendTo('body')
.html(message)
.dialog({
modal: true,
title: title,
autoOpen: false,
width: 400,
resizable: false,
sticky: true,
closeOnEscape: true,
dialogClass: "hm-confirm",
buttons: [
{
class: 'button button--primary',
text: Drupal.t('Yes'),
click: function () {
proceed = true;
$(this).dialog('close');
}
},
{
class: 'button',
text: Drupal.t('No'),
click: function () {
$(this).dialog('close');
}
}
],
close: function () {
proceed ? accept() : deny();
}
});
return modalConfirmationForm.dialog('open');
}
})(jQuery, Drupal, once);
<?php
namespace Drupal\hierarchy_manager\Controller;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityRepository;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Menu\MenuLinkManagerInterface;
use Drupal\Core\Menu\MenuLinkTreeInterface;
use Drupal\Core\Menu\MenuTreeParameters;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Menu item feeding controller.
*/
class HmMenuController extends ControllerBase {
/**
* CSRF Token.
*
* @var \Drupal\Core\Access\CsrfTokenGenerator
*/
protected $csrfToken;
/**
* The menu_link_content storage handler.
*
* @var \Drupal\menu_link_content\MenuLinkContentStorageInterface
*/
protected $storageController;
/**
* The hierarchy manager plugin type manager.
*
* @var \Drupal\hierarchy_manager\PluginTypeManager
*/
protected $hmPluginTypeManager;
/**
* The menu tree service.
*
* @var \Drupal\Core\Menu\MenuLinkTreeInterface
*/
protected $menuTree;
/**
* The menu tree array.
*
* @var array
*/
protected $overviewTree = [];
/**
* The menu link manager.
*
* @var \Drupal\Core\Menu\MenuLinkManagerInterface
*/
protected $menuLinkManager;
/**
* The entity repository object.
*
* @var \Drupal\Core\Entity\EntityRepository
*/
protected $entityRepository;
/**
* {@inheritdoc}
*/
public function __construct(CsrfTokenGenerator $csrfToken, EntityTypeManagerInterface $entity_type_manager, $plugin_type_manager, MenuLinkTreeInterface $menu_tree, MenuLinkManagerInterface $menu_link_manager, EntityRepository $entity_repository) {
$this->csrfToken = $csrfToken;
$this->entityTypeManager = $entity_type_manager;
$this->storageController = $entity_type_manager->getStorage('menu_link_content');
$this->hmPluginTypeManager = $plugin_type_manager;
$this->menuTree = $menu_tree;
$this->menuLinkManager = $menu_link_manager;
$this->entityRepository = $entity_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('csrf_token'),
$container->get('entity_type.manager'),
$container->get('hm.plugin_type_manager'),
$container->get('menu.link_tree'),
$container->get('plugin.manager.menu.link'),
$container->get('entity.repository')
);
}
/**
* Callback for menu tree json.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* Http request object.
* @param string $mid
* Menu ID.
*/
public function menuTreeJson(Request $request, string $mid) {
// Access token.
$token = $request->get('token');
if (empty($token) || !$this->csrfToken->validate($token, $mid)) {
return new Response($this->t('Access denied!'));
}
$parent = $request->get('parent');
$depth = $request->get('depth');
$destination = $request->get('destination');
if (empty($depth)) {
$depth = 0;
}
else {
$depth = intval($depth);
}
if (empty($parent)) {
$parent = '';
}
// We indicate that a menu administrator is running the menu access check.
$request->attributes->set('_menu_admin', TRUE);
$tree = $this->loadMenuTree($mid, $parent, $depth, $destination);
// Menu access check done.
$request->attributes->set('_menu_admin', FALSE);
if ($tree) {
// Display plugin instance.
$display_plugin = $this->getDisplayPlugin();
if (empty($display_plugin)) {
return new JsonResponse(['result' => 'Display profile has not been set up.']);
}
if (method_exists($display_plugin, 'treeData')) {
// Transform the tree data to the structure
// that display plugin accepts.
$tree_data = $display_plugin->treeData($tree);
}
else {
$tree_data = $tree;
}
return new JsonResponse($tree_data);
}
return new JsonResponse([]);
}
/**
* Callback for taxonomy tree json.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* Http request object.
* @param string $mid
* Menu ID.
*/
public function updateMenuLinks(Request $request, string $mid) {
// Access token.
$token = $request->get('token');
if (empty($token) || !$this->csrfToken->validate($token, $mid)) {
return new Response($this->t('Access denied!'));
}
$old_position = (int) $request->get('old_position');
$target_position = $request->get('target');
$parent = $request->get('parent');
$updated_links = $request->get('keys');
$all_siblings = [];
if (is_array($updated_links) && !empty($updated_links)) {
if (empty($parent)) {
// Root is the parent.
$parent = '';
// All children menu links (depth = 1).
$parent_links = $children = $this->loadMenuLinkObjs($mid, $parent, 1);
}
else {
$parent_links = $this->loadMenuLinkObjs($mid, $parent, 1);
}
if (empty($parent_links)) {
// The parent menu doesn't exist.
return new JsonResponse(['result' => 'fail']);
}
if (empty($children)) {
$parent_link = reset($parent_links);
$children = $parent_link->subtree;
}
if ($children) {
// The parent menu has children.
$target_position = intval($target_position);
foreach ($children as $child) {
$link = $child->link;
$link_id = $link->getPluginId();
$all_siblings[$link_id] = $link->getWeight();
}
}
// In order to make room for menu links inserted,
// we need to move all children links forward,
// and work out the weight for links inserted.
$new_hierarchy = $this->hmPluginTypeManager->updateHierarchy($target_position, $all_siblings, $updated_links, $old_position);
// Update all links need to update.
foreach ($new_hierarchy as $link_id => $link_weight) {
$this->menuLinkManager->updateDefinition($link_id, ['weight' => $link_weight, 'parent' => $parent]);
}
$result = [
'result' => 'success',
'updated_nodes' => $new_hierarchy,
];
return new JsonResponse($result);
}
return new JsonResponse(['result' => 'fail']);
}
/**
* Get a display plugin instance.
*
* @return null|object
* The display plugin instance.
*/
protected function getDisplayPlugin() {
$display_profile = $this->hmPluginTypeManager->getDisplayProfile('hm_setup_menu');
return $this->hmPluginTypeManager->getDisplayPluginInstance($display_profile);
}
/**
* Load menu links into one array.
*
* @param string $mid
* The menu ID.
* @param string $parent
* Parent id.
* @param int $depth
* The max depth loaded.
* @param string $destination
* The destination of edit link.
*/
protected function loadMenuTree(string $mid, string $parent, int $depth = 0, string $destination = '') {
$tree = $this->loadMenuLinkObjs($mid, $parent, $depth);
// Load all menu links into one array.
$tree = $this->buildMenuLinkArray($tree);
$links = [];
foreach ($tree as $element) {
if (!empty($destination)) {
$element['url'] = $element['url'] . '?destination=' . $destination;
}
$links[] = $this->hmPluginTypeManager->buildHierarchyItem(
$element['id'],
$element['title'],
$element['parent'],
$element['url'],
$element['status'],
$element['weight']
);
}
return $links;
}
/**
* Load menu links into one array.
*
* @param string $mid
* The menu ID.
* @param string $parent
* Parent id.
* @param int $depth
* The max depth loaded.
*/
protected function loadMenuLinkObjs(string $mid, string $parent, int $depth = 0) {
$menu_para = new MenuTreeParameters();
if (!empty($depth)) {
$menu_para->setMaxDepth($depth);
}
if (!empty($parent)) {
$menu_para->setRoot($parent);
}
$tree = $this->menuTree->load($mid, $menu_para);
$manipulators = [
['callable' => 'menu.default_tree_manipulators:checkAccess'],
['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
];
return $tree = $this->menuTree->transform($tree, $manipulators);
}
/**
* Recursive helper function for loadMenuTree().
*
* @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
* The tree retrieved by \Drupal\Core\Menu\MenuLinkTreeInterface::load().
*
* @return array
* The menu links array.
*/
protected function buildMenuLinkArray($tree) {
// $tree_access_cacheability = new CacheableMetadata();
foreach ($tree as $element) {
// $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
// Only load accessible links.
if (!$element->access->isAllowed()) {
continue;
}
/** @var \Drupal\Core\Menu\MenuLinkInterface $link */
$link = $element->link;
if ($link) {
$id = $link->getPluginId();
$this->overviewTree[$id]['id'] = $id;
$this->overviewTree[$id]['status'] = $link->isEnabled();
if (!$link->isEnabled()) {
$this->overviewTree[$id]['title'] = $link->getTitle() . ' <span class="hm-tree-label hm-tree-label--disabled">(' . $this->t('disabled') . ')</span>';
}
// @todo Remove this in https://www.drupal.org/node/2568785.
elseif ($id === 'user.logout') {
$this->overviewTree[$id]['title'] = $link->getTitle() . ' <span class="hm-tree-label hm-tree-label--login">(' . $this->t('<q>Log in</q> for anonymous users') . ')</span>';
}
// @todo Remove this in https://www.drupal.org/node/2568785.
elseif (($url = $link->getUrlObject()) && $url->isRouted() && $url->getRouteName() == 'user.page') {
$this->overviewTree[$id]['title'] = $link->getTitle() . ' <span class="hm-tree-label hm-tree-label--logged-only">(' . $this->t('logged in users only') . ')</span>';
}
else {
$this->overviewTree[$id]['title'] = $link->getTitle();
}
$parent_id = $link->getParent();
$this->overviewTree[$id]['parent'] = $parent_id;
$this->overviewTree[$id]['weight'] = $link->getWeight();
// Build the edit url.
// Allow for a custom edit link per plugin.
$edit_route = $link->getEditRoute();
if ($edit_route) {
$this->overviewTree[$id]['url'] = $edit_route->toString();
}
else {
// Fall back to the standard edit link.
$this->overviewTree[$id]['url'] = Url::fromRoute('menu_ui.link_edit', ['menu_link_plugin' => $link->getPluginId()])->toString();
}
}
if ($element->subtree) {
$this->buildMenuLinkArray($element->subtree);
}
}
return $this->overviewTree;
}
}
......@@ -2,18 +2,20 @@
namespace Drupal\hierarchy_manager\Controller;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\CsrfTokenGenerator;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\taxonomy\VocabularyInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\taxonomy\Entity\Term;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
/**
* Taxononmy controller class.
* Taxonomy feeding controller class.
*/
class HmTaxonomyController extends ControllerBase {
......@@ -32,29 +34,29 @@ class HmTaxonomyController extends ControllerBase {
protected $storageController;
/**
* Display plugin manager.
* The hierarchy manager plugin type manager.
*
* @var \Drupal\hierarchy_manager\Plugin\HmDisplayPluginInterface
* @var \Drupal\hierarchy_manager\PluginTypeManager
*/
protected $displayManager;
protected $hmPluginTypeManager;
/**
* Setup plugin manager.
* The entity repository.
*
* @var \Drupal\hierarchy_manager\Plugin\HmSetupPluginManager
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $setupManager;
protected $entityRepository;
/**
* {@inheritdoc}
*/
public function __construct(CsrfTokenGenerator $csrfToken, EntityTypeManagerInterface $entity_type_manager, $display_manager, $setup_manager) {
public function __construct(CsrfTokenGenerator $csrfToken, EntityTypeManagerInterface $entity_type_manager, EntityRepositoryInterface $entity_repository, $plugin_type_manager) {
$this->csrfToken = $csrfToken;
$this->entityTypeManager = $entity_type_manager;
$this->entityRepository = $entity_repository;
$this->storageController = $entity_type_manager->getStorage('taxonomy_term');
$this->displayManager = $display_manager;
$this->setupManager = $setup_manager;
$this->hmPluginTypeManager = $plugin_type_manager;
}
/**
......@@ -64,11 +66,26 @@ class HmTaxonomyController extends ControllerBase {
return new static(
$container->get('csrf_token'),
$container->get('entity_type.manager'),
$container->get('plugin.manager.hm.display_plugin'),
$container->get('plugin.manager.hm.hmsetup')
$container->get('entity.repository'),
$container->get('hm.plugin_type_manager')
);
}
/**
* Access check callback for taxonomy tree json.
*
* @param \Drupal\Core\Session\AccountInterface $account
* User account.
* @param string $vid
* Vocabulary ID.
*/
public function access(AccountInterface $account, string $vid) {
if ($account->hasPermission('administer taxonomy')) {
return AccessResult::allowed();
}
return AccessResult::allowedIfHasPermission($account, "edit terms in {$vid}");
}
/**
* Callback for taxonomy tree json.
*
......@@ -82,49 +99,147 @@ class HmTaxonomyController extends ControllerBase {
$token = $request->get('token');
// The term array will be returned.
$term_array = [];
// Store the number of each term id present.
$ids = [];
// Store terms that have ambiguous parents.
$am_terms = [];
// Store all terms only have single ancestor.
$single_parent = [];
if (empty($token) || !$this->csrfToken->validate($token, $vid)) {
return new Response($this->t('Access denied!'));
}
$parent = $request->get('parent') ?: 0;
$depth = $request->get('depth') ?: 1;
$depth = $request->get('depth');
$destination = $request->get('destination');
$vocabulary_hierarchy = $this->storageController->getVocabularyHierarchyType($vid);
// Taxonomy tree must not be multiple parent tree.
if ($vocabulary_hierarchy !== VocabularyInterface::HIERARCHY_MULTIPLE) {
$tree = $this->storageController->loadTree($vid, $parent, $depth, TRUE);
if (!empty($depth)) {
$depth = intval($depth);
}
$access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term');
$tree = $this->storageController->loadTree($vid, $parent, $depth, TRUE);
$access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term');
foreach ($tree as $term) {
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,
'edit_url' => $term->toUrl('edit-form')->toString(),
];
foreach ($tree as $term) {
if ($term instanceof Term) {
$term = $this->entityRepository->getTranslationFromContext($term);
// User can only access the terms that they can update.
if ($access_control_handler->access($term, 'update')) {
if (empty($destination)) {
$url = $term->toUrl('edit-form')->toString();
}
else {
$url = $term->toUrl('edit-form', ['query' => ['destination' => $destination]])->toString();
}
$term_parent = $term->parents;
$id = $term->id();
$count_parent = count($term_parent);
if (isset($ids[$id])) {
if ($ids[$id] === 0 && isset($single_parent[$id])) {
// Update previous term in the term array
// which has the same ID. Make it not draggable.
$term_array[$single_parent[$id]]['draggable'] = FALSE;
}
$ids[$id]++;
$term_id = $id . '_' . $ids[$id];
}
else {
$ids[$id] = 0;
$term_id = $id;
}
// If a taxonomy term has multiple parents,
// It will present multiple times under different parents.
// So the term id will be duplicated.
// The solution is to format the term id as following,
// {term_id}_{parent_index}.
if ($count_parent > 1) {
$draggable = FALSE;
// This term has an ancestor with multiple parents.
if ($ids[$id] === $count_parent) {
// Put into the ambiguous array.
// Will solve it later.
$am_terms[] = [
'solved' => FALSE,
'id' => $term_id,
'label' => $term->label(),
'parent' => $term_parent,
'url' => $url,
'publish' => $term->isPublished(),
'weight' => $term->getWeight(),
'draggable' => FALSE,
];
continue;
}
$parent_id = $term_parent[$ids[$id]];
}
else {
if ($ids[$id]) {
// The parent has multiple grandparent.
$parent_id = $term_parent[0] . '_' . $ids[$id];
$draggable = FALSE;
}
else {
// The parent doesn't have multiple grandparent.
$parent_id = $term_parent[0];
$draggable = TRUE;
// At this point, we still don't know
// if this term has multiple ancestors or not.
// So keep the index of term array for later update
// if needed.
$single_parent[$id] = count($term_array);
}
}
$term_array[] = $this->hmPluginTypeManager->buildHierarchyItem(
$term_id,
$term->label(),
$parent_id,
$url,
$term->isPublished(),
$term->getWeight(),
$draggable
);
}
}
}
// Figure out the parent id for terms in the ambiguous term array.
do {
$found = FALSE;
foreach ($am_terms as $key => $term) {
if ($term['solved']) {
continue;
}
$parent_ids = $term['parent'];
foreach ($parent_ids as $id) {
if ($ids[$id]) {
// Found the parent with multiple grandparent.
$term_array[] = $this->hmPluginTypeManager->buildHierarchyItem(
$term['id'],
$term['label'],
$id . '_' . $ids[$id],
$term['url'],
$term['publish'],
$term['weight'],
$term['draggable']
);
$found = TRUE;
// Remove this parent from ids array.
$ids[$id]--;
$am_terms[$key]['solved'] = TRUE;
continue 2;
}
}
}
} while ($found);
// Taxonomy setup plugin instance.
$taxonomy_setup_plugin = $this->setupManager->createInstance('hm_setup_taxonomy');
// Display profile.
$display_profile = $this->entityTypeManager->getStorage('hm_display_profile')->load($taxonomy_setup_plugin->getDispalyProfileId());
// Display plugin ID.
$display_plugin_id = $display_profile->get("plugin");
$display_profile = $this->hmPluginTypeManager->getDisplayProfile('hm_setup_taxonomy');
// Display plugin instance.
$display_plugin = $this->displayManager->createInstance($display_plugin_id);
$display_plugin = $this->hmPluginTypeManager->getDisplayPluginInstance($display_profile);
if (empty($display_plugin)) {
return new JsonResponse(['result' => 'Display profile has not been set up.']);
}
if (method_exists($display_plugin, 'treeData')) {
// Convert the tree data to the structure
......@@ -153,99 +268,77 @@ class HmTaxonomyController extends ControllerBase {
return new Response($this->t('Access denied!'));
}
$target_id = $request->get('target');
$parent_id = intval($request->get('parent'));
$mode = $request->get('mode');
$target_position = $request->get('target');
$old_position = (int) $request->get('old_position');
$old_parent_id = $request->get('old_parent');
// Remove the parent index from the parent id.
$old_parent_id = explode('_', $old_parent_id)[0];
$parent_id = $request->get('parent');
// Remove the parent index from the parent id.
$parent_id = explode('_', $parent_id)[0];
$updated_terms = $request->get('keys');
$success = FALSE;
$all_siblings = [];
if (is_array($updated_terms) && !empty($updated_terms) && !empty($target_id)) {
if (is_array($updated_terms) && !empty($updated_terms)) {
// Remove the parent index from the term id.
for ($i = 0; $i < count($updated_terms); $i++) {
$updated_terms[$i] = explode('_', $updated_terms[$i])[0];
}
// 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);
}
}
// 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'.
// Children of the parent term in weight and name alphabetically order.
$children = $this->storageController->loadTree($vid, $parent_id, 1);
if (!empty($children)) {
// The parent term has children.
$target_position = intval($target_position);
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;
}
}
else {
$step = count($updated_terms) + $weight;
}
$all_siblings[$child->tid] = (int) $child->weight;
}
}
if (!isset($weight)) {
// Set the weight to 0 as default.
$weight = 0;
}
$new_hierarchy = $this->hmPluginTypeManager->updateHierarchy($target_position, $all_siblings, $updated_terms, $old_position);
$tids = array_keys($new_hierarchy);
$terms = Term::loadMultiple($updated_terms);
// Update all terms, the weight will be increased by 1,
// after inserting.
// Load all terms needed to update.
$terms = Term::loadMultiple($tids);
// Update all terms.
foreach ($terms as $term) {
if ($access_control_handler->access($term, 'update')) {
$term->set('parent', ['target_id' => $parent_id]);
$term->setWeight($weight++);
$term->setWeight($new_hierarchy[$term->id()]);
// Update the parent IDs.
if (in_array($term->id(), $updated_terms)) {
$parents = [];
$same_parent = $old_parent_id === $parent_id;
// Update the parent only if it is changed.
if (!$same_parent) {
foreach ($term->get('parent') as $parent) {
$tid = $parent->get('target_id')->getValue();
if ($tid === $old_parent_id) {
$tid = $parent_id;
}
elseif ($tid === $parent_id) {
continue;
}
$parents[] = ['target_id' => $tid];
}
// Set the new parent.
$term->set('parent', $parents);
}
}
$success = $term->save();
}
}
}
if ($success) {
return new JsonResponse(['result' => 'success']);
}
$result = [
'result' => $success ? 'success' : 'fail',
'updated_nodes' => $new_hierarchy,
];
return new JsonResponse(['result' => 'fail']);
return new JsonResponse($result);
}
}
......@@ -9,7 +9,7 @@ use Drupal\Core\Config\Entity\ConfigEntityBase;
*
* @ConfigEntityType(
* id = "hm_display_profile",
* label = @Translation("HM Display Profile Entity"),
* label = @Translation("HM Display Profile"),
* handlers = {
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "list_builder" = "Drupal\hierarchy_manager\HmDisplayProfileListBuilder",
......@@ -33,6 +33,8 @@ use Drupal\Core\Config\Entity\ConfigEntityBase;
* "id",
* "label",
* "plugin",
* "config",
* "confirm",
* },
* links = {
* "canonical" = "/admin/structure/hm_display_profile/{hm_display_profile}",
......@@ -66,4 +68,18 @@ class HmDisplayProfile extends ConfigEntityBase implements HmDisplayProfileInter
*/
protected $plugin;
/**
* The configurations.
*
* @var string
*/
protected $config;
/**
* The confirmation option.
*
* @var bool
*/
protected $confirm = FALSE;
}