diff --git a/README.md b/README.md index ae737b59e5a195fe6b17fbfceb7f518b08799778..61455023072872a19bbbab15b6a842dc2bb68062 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ 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 romte CDN if it wasn't hosted locally under /libraries/jquery.jstree/3.3.8/ folder) INSTALLATION ------------ diff --git a/hierarchy_manager.module b/hierarchy_manager.module index 0f5bcb8c94ffd6d95c4c1d1c54a93bf7c549c2a9..574aece1856a370234200771d11d33998090e944 100644 --- a/hierarchy_manager.module +++ b/hierarchy_manager.module @@ -29,6 +29,18 @@ function hierarchy_manager_library_info_alter(array &$libraries, $module) { } } +/** + * Implement hook_entity_type_alter(). + * + * @param array $entity_types + * Entity type information array. + */ +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. * diff --git a/hierarchy_manager.routing.yml b/hierarchy_manager.routing.yml index ca0b1727d02e71f2dbc7c30a86b93fc94ffb70c7..38b2518ae4885738cd12016bf2698fcff7944eb1 100644 --- a/hierarchy_manager.routing.yml +++ b/hierarchy_manager.routing.yml @@ -9,7 +9,7 @@ hierarchy_manager.hm_config_form: options: _admin_route: TRUE -# Taxonomy display plugin. +# Taxonomy hierarchy plugin. hierarchy_manager.taxonomy.tree.json: path: '/admin/hierarchy_manager/taxonomy/json/{vid}' defaults: @@ -29,3 +29,22 @@ hierarchy_manager.taxonomy.tree.update: 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 diff --git a/hierarchy_manager.services.yml b/hierarchy_manager.services.yml index f65c06e80f514220752ecebc3be491edf45260a9..a3b266688ff016d5c30fa6d1e8b7b0f171211f43 100644 --- a/hierarchy_manager.services.yml +++ b/hierarchy_manager.services.yml @@ -1,11 +1,19 @@ 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 } diff --git a/js/Plugin/jstree/hm.jstree.js b/js/Plugin/jstree/hm.jstree.js index 69ad563898868e09648104f0f43794a865309d47..62eea15e4da5c30cdc127f837bd7c04fabe55262 100644 --- a/js/Plugin/jstree/hm.jstree.js +++ b/js/Plugin/jstree/hm.jstree.js @@ -20,6 +20,7 @@ const updateURL = treeContainer.attr('url-update') let reload = true; let rollback = false; + let after = 1; // Ajax callback to refresh the tree. if (reload) { // Build the tree. @@ -54,12 +55,26 @@ const thisTree = data.instance; const movedNode = data.node; - if (!rollback) { + if (!rollback) { + let list = thisTree.get_node(data.parent).children; + let before = ''; + let after = ''; + if (data.position > 0) { + before = list[data.position - 1]; + } + + if (data.position < list.length - 1) { + after = list[data.position + 1]; + } + + let parent = data.parent === '#' ? 0 : data.parent; // Update the data on server side. $.post(updateURL, { keys: [movedNode.id], target: data.position, - parent: data.parent + parent: parent, + after: after, + before: before }) .done(function(response) { if (response.result !== "success") { diff --git a/src/Controller/HmMenuController.php b/src/Controller/HmMenuController.php new file mode 100644 index 0000000000000000000000000000000000000000..9f2793a10c27ac5d47bd0821be594fe44998b812 --- /dev/null +++ b/src/Controller/HmMenuController.php @@ -0,0 +1,372 @@ +<?php + +namespace Drupal\hierarchy_manager\Controller; + +use Drupal\Core\Url; +use Drupal\Core\Access\CsrfTokenGenerator; +use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Menu\MenuLinkTreeInterface; +use Drupal\Core\Menu\MenuLinkManagerInterface; +use Drupal\Core\Menu\MenuTreeParameters; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\JsonResponse; + +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; + + /** + * {@inheritdoc} + */ + public function __construct(CsrfTokenGenerator $csrfToken, EntityTypeManagerInterface $entity_type_manager, $plugin_type_manager, MenuLinkTreeInterface $menu_tree, MenuLinkManagerInterface $menu_link_manager) { + $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; + } + + /** + * {@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') + ); + } + + /** + * 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 = ''; + } + + if (empty($destination)) { + $destination = ''; + } + // 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 $vid + * Vocabulary 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!')); + } + + $target_position = $request->get('target'); + $parent = $request->get('parent'); + $updated_links = $request->get('keys'); + //$after = $request->get('after'); + $before = $request->get('before'); + + if (is_array($updated_links) && !empty($updated_links)) { + if (empty($parent)) { + // Root is the parent. + $parent = ''; + $parent_links = $children = $this->loadMenuLinkObjs($mid, $parent, 1); + } + else { + // All children menu links (depth = 1). + $parent_links = $this->loadMenuLinkObjs($mid, $parent, 1); + } + // 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. + if (empty($parent_links)) { + // The parent menu doesn't exist. + return new JsonResponse(['result' => 'fail']); + } + else { + if (empty($children)) { + $parent_link = reset($parent_links); + $children = $parent_link->subtree; + } + + if ($children) { + // The parent menu has children. + $target_position = intval($target_position); + $all_siblings = []; + $insert_after = TRUE; + $position = 0; + foreach ($children as $child) { + $link = $child->link; + $link_id = $link->getPLuginId(); + // Figure out if the new links are inserted + // after the target position. + if ($position++ == $target_position && $link_id !== $before) { + $insert_after = FALSE; + } + + $all_siblings[$link_id] = (int) $link->getWeight(); + } + + $new_hierarchy = $this->hmPluginTypeManager->updateHierarchy($target_position, $all_siblings, $updated_links, $insert_after); + $weight = $new_hierarchy['start_weight']; + $moving_siblings = $new_hierarchy['moving_siblings']; + + // Update all sibling links needed to update. + foreach ($moving_siblings as $link_id => $link_weight) { + $this->menuLinkManager->updateDefinition($link_id, ['weight' => $link_weight]); + } + } + else { + // The parent link doesn't have children. + $weight = 0; + } + // Move all links updated. + foreach ($updated_links as $link_id) { + $this->menuLinkManager->updateDefinition($link_id, ['weight' => $weight++, 'parent' => $parent]); + } + } + + return new JsonResponse(['result' => 'success']); + } + + return new JsonResponse(['result' => 'fail']); + } + + /** + * Get a display plugin instance. + * + * @return NULL|object + */ + protected function getDisplayPlugin() { + return $this->hmPluginTypeManager->getDisplayPluginInstance('hm_setup_menu'); + } + + /** + * 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']); + } + + 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. + * @param string $destination + * The destination of edit link. + */ + 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) { + // The id consistes of plugin ID and link ID. + $id = $link->getPluginId(); + $this->overviewTree[$id]['id'] = $id; + if (!$link->isEnabled()) { + $this->overviewTree[$id]['title'] = '(' . $this->t('disabled') . ')' . $link->getTitle(); + } + // @todo Remove this in https://www.drupal.org/node/2568785. + elseif ($id === 'user.logout') { + $this->overviewTree[$id]['title'] = ' (' . $this->t('<q>Log in</q> for anonymous users') . ')' . $link->getTitle(); + } + // @todo Remove this in https://www.drupal.org/node/2568785. + elseif (($url = $link->getUrlObject()) && $url->isRouted() && $url->getRouteName() == 'user.page') { + $this->overviewTree[$id]['title'] = ' (' . $this->t('logged in users only') . ')' . $link->getTitle(); + } + else { + $this->overviewTree[$id]['title'] = $link->getTitle(); + } + + $this->overviewTree[$id]['parent'] = $link->getParent(); + // 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); + } + } + + /* $tree_access_cacheability + ->merge(CacheableMetadata::createFromRenderArray($this->overviewTree)) + ->applyTo($form); */ + + return $this->overviewTree; + } +} + diff --git a/src/Controller/HmTaxonomyController.php b/src/Controller/HmTaxonomyController.php index e0992b1370923d25e9d02e02d2219d0e90757e0b..68cc884c34e87b50a28be4fdf4fdd4d5d809e0f2 100644 --- a/src/Controller/HmTaxonomyController.php +++ b/src/Controller/HmTaxonomyController.php @@ -30,31 +30,23 @@ class HmTaxonomyController extends ControllerBase { * @var \Drupal\taxonomy\TermStorageInterface */ protected $storageController; - + /** - * Display plugin manager. + * The hierarchy manager plugin type manager. * - * @var \Drupal\hierarchy_manager\Plugin\HmDisplayPluginInterface + * @var \Drupal\hierarchy_manager\PluginTypeManager */ - protected $displayManager; - - /** - * Setup plugin manager. - * - * @var \Drupal\hierarchy_manager\Plugin\HmSetupPluginManager - */ - protected $setupManager; + protected $hmPluginTypeManager; /** * {@inheritdoc} */ - public function __construct(CsrfTokenGenerator $csrfToken, EntityTypeManagerInterface $entity_type_manager, $display_manager, $setup_manager) { + public function __construct(CsrfTokenGenerator $csrfToken, EntityTypeManagerInterface $entity_type_manager, $plugin_type_manager) { $this->csrfToken = $csrfToken; $this->entityTypeManager = $entity_type_manager; $this->storageController = $entity_type_manager->getStorage('taxonomy_term'); - $this->displayManager = $display_manager; - $this->setupManager = $setup_manager; + $this->hmPluginTypeManager = $plugin_type_manager; } /** @@ -64,8 +56,7 @@ 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('hm.plugin_type_manager') ); } @@ -99,31 +90,28 @@ class HmTaxonomyController extends ControllerBase { $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')) { - $term_array[] = [ - 'id' => $term->id(), - 'text' => $term->label(), - 'parent' => $term->parents[0], - 'edit_url' => $term->toUrl('edit-form')->toString(), - ]; + $term_array[] = $this->hmPluginTypeManager->buildHierarchyItem( + $term->id(), + $term->label(), + $term->parents[0], + $term->toUrl('edit-form')->toString()); } } } } - // 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 plugin instance. - $display_plugin = $this->displayManager->createInstance($display_plugin_id); + $display_plugin = $this->hmPluginTypeManager->getDisplayPluginInstance('hm_setup_taxonomy'); + + 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,8 +141,10 @@ class HmTaxonomyController extends ControllerBase { } $target_position = $request->get('target'); - $parent_id = intval($request->get('parent')); + $parent_id = $request->get('parent'); $updated_terms = $request->get('keys'); + //$after = $request->get('after'); + $before = $request->get('before'); $success = FALSE; if (is_array($updated_terms) && !empty($updated_terms)) { @@ -175,36 +165,34 @@ class HmTaxonomyController extends ControllerBase { } else { // The parent term has children. - $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 { - // There is planty room, no need to move anymore. - break; - } + $target_position = intval($target_position); + $all_siblings = []; + $insert_after = TRUE; + $position = 0; + + foreach ($children as $child) { + // Figure out if the new links are inserted + // after the target position. + if ($position++ == $target_position && $child->tid !== $before) { + $insert_after = FALSE; } - $step = $weight + count($updated_terms); + + $all_siblings[$child->tid] = (int) $child->weight; + } + + $new_hierarchy = $this->hmPluginTypeManager->updateHierarchy($target_position, $all_siblings, $updated_terms, $insert_after); + $weight = $new_hierarchy['start_weight']; + $moving_siblings = $new_hierarchy['moving_siblings']; + $tids = array_keys($moving_siblings); + + if (!empty($tids)) { + // Update siblings. $term_siblings = Term::loadMultiple($tids); foreach ($term_siblings as $term) { - $term->setWeight($step++); + $term->setWeight($moving_siblings[$term->id()]); $success = $term->save(); } } - 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. diff --git a/src/Form/HmMenuForm.php b/src/Form/HmMenuForm.php new file mode 100644 index 0000000000000000000000000000000000000000..0476df8abe386d0fd624bffd5540aeb03245b6a8 --- /dev/null +++ b/src/Form/HmMenuForm.php @@ -0,0 +1,145 @@ +<?php + +namespace Drupal\hierarchy_manager\Form; + +use Drupal\Core\Form\FormStateInterface; +use Drupal\menu_ui\MenuForm; + +class HmMenuForm extends MenuForm { + + /** + * The indicator if the menu hierarchy manager is enabled. + * + * @var bool|NULL + */ + private $isEnabled = NULL; + + /** + * The hierarchy manager plugin type manager. + * + * @var \Drupal\hierarchy_manager\PluginTypeManager + */ + private $hmPluginTypeManager = NULL; + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + // If the menu hierarchy manager plugin is enabled. + // Override the menu overview form. + if ($this->isMenuPluginEnabled() && $this->loadPluginManager()) { + $menu = $this->entity; + + // Add menu links administration form for existing menus. + if (!$menu->isNew() || $menu->isLocked()) { + // We are removing the menu link overview form + // and using our own hierarchy manager tree instead. + // The overview form implemented by Drupal Menu UI module + // @see \Drupal\menu_ui\MenuForm::form() + unset($form['links']); + $form['hm_links'] = $this->buildOverviewTree([], $form_state); + } + } + + return $form; + } + + /** + * Submit handler for the menu overview form. + * + * The hierarchy manager tree is a pure front-end solution in which + * we don't need to deal with the submission data from the back-end. + * Therefore nothing need to do, + * if the menu hierarchy plugin is enabled. + */ + protected function submitOverviewForm(array $complete_form, FormStateInterface $form_state) { + if (!$this->isMenuPluginEnabled()) { + parent::submitOverviewForm($complete_form, $form_state); + } + } + + /** + * Build a menu links overview tree element. + * + * @param array $form + * Parent form array. + * @param FormStateInterface $form_state + * Form state object. + * @return NULL|array + */ + protected function buildOverviewTree(array $form, FormStateInterface $form_state) { + global $base_path; + + $display_plugin_instance = $this->hmPluginTypeManager->getDisplayPluginInstance('hm_setup_menu'); + + if (!empty($display_plugin_instance)) { + if (method_exists($display_plugin_instance, 'getForm')) { + // Menu ID. + $mid = $this->entity->id(); + // CSRF token. + $token = \Drupal::csrfToken()->get($mid); + // Get current language. + $language = \Drupal::languageManager()->getCurrentLanguage(); + // Destination for edit link. + $destination = $this->getDestinationArray(); + if (isset($destination['destination'])) { + $destination = $destination['destination']; + } + else { + $destination = ''; + } + if ($language->isDefault()) { + $source_url = $base_path . 'admin/hierarchy_manager/menu/json/' . $mid . '?token=' . $token . '&destination=' . $destination; + $update_url = $base_path . 'admin/hierarchy_manager/menu/update/' . $mid . '?token=' . $token; + } + else { + $source_url = $base_path . $language->getId() . '/admin/hierarchy_manager/menu/json/' . $mid . '?token=' . $token . '&destination=' . $destination; + $update_url = $base_path . $language->getId() . '/admin/hierarchy_manager/menu/update/' . $mid . '?token=' . $token; + } + return $display_plugin_instance->getForm($source_url, $update_url, $form, $form_state); + } + } + + return []; + } + + /** + * Create a hierarchy manager plugin manager. + * + * @return \Drupal\hierarchy_manager\PluginTypeManager + */ + protected function loadPluginManager() { + if (empty($this->hmPluginTypeManager)) { + $this->hmPluginTypeManager = \Drupal::service('hm.plugin_type_manager'); + } + + return $this->hmPluginTypeManager; + } + + /** + * Check if the menu hierarchy plugin is enabled. + * + * @return boolean|NULL + * Return TRUE if the menu plugin is enabled, + * otherwise return FALSE. + */ + protected function isMenuPluginEnabled() { + if ($this->isEnabled === NULL) { + if ($config = \Drupal::config('hierarchy_manager.hmconfig')) { + if ($allowed_setup_plugins = $config->get('allowed_setup_plugins')) { + if (!empty($allowed_setup_plugins['hm_setup_menu'])) { + $this->isEnabled = TRUE; + } + else { + $this->isEnabled = FALSE; + } + } + } + } + + return $this->isEnabled; + } +} + diff --git a/src/Plugin/HmDisplayPlugin/HmDisplayJstree.php b/src/Plugin/HmDisplayPlugin/HmDisplayJstree.php index c5588bac45800235050b67b09ec38f88d5845c04..ea26592bfb8283c613ac4030952cae20f9c2ca63 100644 --- a/src/Plugin/HmDisplayPlugin/HmDisplayJstree.php +++ b/src/Plugin/HmDisplayPlugin/HmDisplayJstree.php @@ -85,7 +85,7 @@ class HmDisplayJstree extends HmDisplayPluginBase implements HmDisplayPluginInte foreach ($data as $tree_node) { $jstree_node = $tree_node; // The root id for jsTree is #. - if ($tree_node['parent'] === '0') { + if (empty($tree_node['parent'])) { $jstree_node['parent'] = '#'; } // Custom data diff --git a/src/Plugin/HmSetupPlugin/HmMenu.php b/src/Plugin/HmSetupPlugin/HmMenu.php new file mode 100644 index 0000000000000000000000000000000000000000..6feb0e7eb6ac7eaa94040ca14a4b602b97844462 --- /dev/null +++ b/src/Plugin/HmSetupPlugin/HmMenu.php @@ -0,0 +1,18 @@ +<?php + +namespace DRupal\hierarchy_manager\Plugin\HmSetupPlugin; + +use Drupal\hierarchy_manager\Plugin\HmSetupPluginInterface; +use Drupal\hierarchy_manager\Plugin\HmSetupPluginBase; + +/** + * Menu link hierarchy setup plugin. + * + * @HmSetupPlugin( + * id = "hm_setup_menu", + * label = @Translation("Menu link hierarchy setup plugin") + * ) + */ +class HmMenu extends HmSetupPluginBase implements HmSetupPluginInterface { +} + diff --git a/src/Plugin/HmSetupPluginBase.php b/src/Plugin/HmSetupPluginBase.php index 84b0558374987d72903802ab7f9bceb88e70096b..09992080022de73e6202000dcef2ce98e0ec8db3 100644 --- a/src/Plugin/HmSetupPluginBase.php +++ b/src/Plugin/HmSetupPluginBase.php @@ -32,8 +32,12 @@ abstract class HmSetupPluginBase extends PluginBase implements HmSetupPluginInte parent::__construct($configuration, $plugin_id, $plugin_definition); $plugin_settings = \Drupal::config('hierarchy_manager.hmconfig')->get('setup_plugin_settings'); - $settings = $plugin_settings[$this->pluginId] ?: []; - $this->displayProfile = $settings['display_profile']; + if (isset($plugin_settings[$this->pluginId])) { + $this->displayProfile = $plugin_settings[$this->pluginId]['display_profile']; + } + else { + $this->displayProfile = ''; + } } /** diff --git a/src/PluginTypeManager.php b/src/PluginTypeManager.php new file mode 100644 index 0000000000000000000000000000000000000000..a6a5a8b3d0f6b5fe23a6b0430ff17ea10f72ca8b --- /dev/null +++ b/src/PluginTypeManager.php @@ -0,0 +1,203 @@ +<?php + +namespace Drupal\hierarchy_manager; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\hierarchy_manager\Plugin\HmDisplayPluginManager; +use Drupal\hierarchy_manager\Plugin\HmSetupPluginManager; + +class PluginTypeManager { + + /** + * Display plugin manager. + * + * @var \Drupal\hierarchy_manager\Plugin\HmDisplayPluginManager + */ + protected $displayManager; + + /** + * Setup plugin manager. + * + * @var \Drupal\hierarchy_manager\Plugin\HmSetupPluginManager + */ + protected $setupManager; + + /** + * The entity type manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * {@inheritdoc} + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager, HmDisplayPluginManager $display_manager, HmSetupPluginManager $setup_manager) { + $this->entityTypeManager = $entity_type_manager; + $this->displayManager = $display_manager; + $this->setupManager = $setup_manager; + } + + /** + * Construct an item inside the hierarchy. + * + * @param string|int $id + * Item id. + * @param string $label + * Item text. + * @param string $parent + * Parent id of the item. + * @param string $edit_url + * The URL where to edit this item. + * @return array + * The hierarchy item array. + */ + public function buildHierarchyItem($id, $label, $parent, $edit_url) { + return + [ + 'id' => $id, + 'text' => $label, + 'parent' => $parent, + 'edit_url' => $edit_url, + ]; + } + + /** + * Get a display plugin instance according to a setup plugin. + * + * @param string $setup_plugin_id + * setup plugin ID. + * @return NULL|object + * The display plugin instance. + */ + public function getDisplayPluginInstance(string $setup_plugin_id) { + // The setup plugin instance. + $setup_plugin = $this->setupManager->createInstance($setup_plugin_id); + // Display profile. + $display_profile = $this->entityTypeManager->getStorage('hm_display_profile')->load($setup_plugin->getDispalyProfileId()); + + if (empty($display_profile)) { + return NULL; + } + + // Display plugin ID. + $display_plugin_id = $display_profile->get("plugin"); + + return $this->displayManager->createInstance($display_plugin_id); + } + + /** + * Update the items for a hierarchy + * + * @param int $target_position + * Which position the new items will be insert. + * @param array $all_siblings + * All siblings of the new items in an array[$item_id => (int)$weight] + * @param array $updated_items + * IDs of new items inserted. + * @param int|bool $after + * Indicator if new items are inserted after target position. + * + * @return array + * All siblings needed to move and their new weights. + */ + public function updateHierarchy(int $target_position, array $all_siblings, array $updated_items, $after) { + $filtered_moving_siblings = []; + $first_half = TRUE; + + $total = count($all_siblings); + if ($target_position === 0) { + // The insert postion is the first position. + // we don't need to move any siblings. + $weight = (int) reset($all_siblings) - 1; + } + elseif ($target_position >= $total - 1) { + // The insert postion is the end, + // we don't need to move any siblings. + $last_item= array_slice($all_siblings, -1, 1, TRUE); + $weight = (int) reset($last_item) + 1; + } + else { + $target_item = array_slice($all_siblings, $target_position, 1, TRUE); + $weight = (int) reset($target_item); + // If the target position is in the second half, + // we will move all siblings + // after the target position forward. + // Otherwise, we will move siblings + // before the target position backwards. + if ($target_position >= $total / 2) { + $first_half = FALSE; + + if ($after) { + // Insert after the target position. + // The target stay where it is. + $weight += 1; + $moving_siblings = array_slice($all_siblings, $target_position + 1, NULL, TRUE); + } + else { + // Insert before the target position. + // The target need to move forwards. + $moving_siblings = array_slice($all_siblings, $target_position, NULL, TRUE); + } + $step = $weight + count($updated_items); + } + else { + if ($after) { + // Insert after the target position. + // The target need to move backwards. + $moving_siblings = array_slice($all_siblings, 0, $target_position + 1, TRUE); + } + else { + // Insert before the target position. + // The target stay where it is. + $weight -= 1; + $moving_siblings = array_slice($all_siblings, 0, $target_position, TRUE); + } + $weight = $step = $weight - count($updated_items); + // Reverse the siblings_moved array + // as we will decrease the weight + // starting from the first element + // and the new weight should be in + // an opposite order. + $moving_siblings = array_reverse($moving_siblings, TRUE); + } + + // Move all siblings that need to move. + foreach($moving_siblings as $item_id => $item_weight) { + // Skip all links in the updated array. They will be moved later. + if (in_array($item_id, $updated_items)) { + continue; + } + if ($first_half) { + // While moving the first half of the siblings, + // all moving siblings' weight are decreased, + // if they are greater than the step. + if ((int)$item_weight < --$step) { + // There is planty room, no need to move anymore. + break; + } + else { + // Update the weight. + $filtered_moving_siblings[$item_id] = $step; + } + } + else { + // While moving the second half of the siblings, + // all moving siblings' weight are increased, + // if they are less than the step. + if ((int)$item_weight < ++$step) { + // Update the weight. + $filtered_moving_siblings[$item_id] = $step; + } + else { + // There is planty room, no need to move anymore. + break; + } + } + } + } + + return ['start_weight' => $weight, 'moving_siblings' => $filtered_moving_siblings]; + } +} +