You are here

taxonomy_menu_ui.module in Taxonomy Menu UI 8

Add ability to create menu links for taxonomy terms.

File

taxonomy_menu_ui.module
View source
<?php

/**
 * @file
 * Add ability to create menu links for taxonomy terms.
 */
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\taxonomy\VocabularyInterface;
use Drupal\taxonomy\TermInterface;
use Drupal\taxonomy\Entity\Vocabulary;

/**
 * Helper function to create or update a menu link for a taxonomy term.
 *
 * @param \Drupal\taxonomy\TermInterface $term
 *   Term entity.
 * @param array $values
 *   Values for the menu link.
 */
function _menu_ui_taxonomy_term_save(TermInterface $term, array $values) {

  /** @var \Drupal\menu_link_content\MenuLinkContentInterface $entity */
  if (!empty($values['entity_id'])) {
    $entity = MenuLinkContent::load($values['entity_id']);
    if ($entity
      ->isTranslatable()) {
      if (!$entity
        ->hasTranslation($term
        ->language()
        ->getId())) {
        $entity = $entity
          ->addTranslation($term
          ->language()
          ->getId(), $entity
          ->toArray());
      }
      else {
        $entity = $entity
          ->getTranslation($term
          ->language()
          ->getId());
      }
    }
  }
  else {

    // Create a new menu_link_content entity.
    $entity = MenuLinkContent::create([
      'link' => [
        'uri' => 'internal:/taxonomy/term/' . $term
          ->id(),
      ],
      'langcode' => $term
        ->language()
        ->getId(),
    ]);
    $entity->enabled->value = 1;
  }
  $entity->title->value = trim($values['title']);
  $entity->description->value = trim($values['description']);
  $entity->menu_name->value = $values['menu_name'];
  $entity->parent->value = $values['parent'];
  $entity->weight->value = isset($values['weight']) ? $values['weight'] : 0;
  $entity
    ->save();
}

/**
 * Returns the definition for a menu link for the given term.
 *
 * @param \Drupal\taxonomy\TermInterface $term
 *   The term entity.
 *
 * @return array
 *   An array that contains default values for the menu link form.
 */
function taxonomy_menu_ui_get_menu_link_defaults(TermInterface $term) {

  // Prepare the definition for the edit form.

  /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */
  $vid = $term
    ->bundle();
  $vocabulary = Vocabulary::load($vid);
  $menu_name = strtok($vocabulary
    ->getThirdPartySetting('menu_ui', 'parent', 'main:'), ':');
  $defaults = FALSE;
  if ($term
    ->id()) {
    $id = FALSE;

    // Give priority to the default menu.
    $vocabulary_menus = $vocabulary
      ->getThirdPartySetting('menu_ui', 'available_menus', [
      'main',
    ]);
    if (in_array($menu_name, $vocabulary_menus)) {
      $query = \Drupal::entityQuery('menu_link_content')
        ->condition('link.uri', 'taxonomy/term/' . $term
        ->id())
        ->condition('menu_name', $menu_name)
        ->sort('id', 'ASC')
        ->range(0, 1);
      $result = $query
        ->execute();
      $id = !empty($result) ? reset($result) : FALSE;
    }

    // Check all allowed menus if a link does not exist in the default menu.
    if (!$id && !empty($vocabulary_menus)) {
      $query = \Drupal::entityQuery('menu_link_content')
        ->condition('link.uri', 'internal:/taxonomy/term/' . $term
        ->id())
        ->condition('menu_name', array_values($vocabulary_menus), 'IN')
        ->sort('id', 'ASC')
        ->range(0, 1);
      $result = $query
        ->execute();
      $id = !empty($result) ? reset($result) : FALSE;
    }
    if ($id) {
      $menu_link = MenuLinkContent::load($id);
      $menu_link = \Drupal::service('entity.repository')
        ->getTranslationFromContext($menu_link);
      $defaults = [
        'entity_id' => $menu_link
          ->id(),
        'id' => $menu_link
          ->getPluginId(),
        'title' => $menu_link
          ->getTitle(),
        'title_max_length' => $menu_link
          ->getFieldDefinitions()['title']
          ->getSetting('max_length'),
        'description' => $menu_link
          ->getDescription(),
        'menu_name' => $menu_link
          ->getMenuName(),
        'parent' => $menu_link
          ->getParentId(),
        'weight' => $menu_link
          ->getWeight(),
      ];
    }
  }
  if (!$defaults) {

    // Get the default max_length of a menu link title from the base field
    // definition.
    $field_definitions = \Drupal::service('entity_field.manager')
      ->getBaseFieldDefinitions('menu_link_content');
    $max_length = $field_definitions['title']
      ->getSetting('max_length');
    $defaults = [
      'entity_id' => 0,
      'id' => '',
      'title' => '',
      'title_max_length' => $max_length,
      'description' => '',
      'menu_name' => $menu_name,
      'parent' => '',
      'weight' => 0,
    ];
  }
  return $defaults;
}

/**
 * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\taxonomy\TermForm.
 *
 * Adds menu item fields to the taxonomy term form.
 *
 * @see taxonomy_menu_ui_form_taxonomy_term_form_submit()
 */
function taxonomy_menu_ui_form_taxonomy_term_form_alter(&$form, FormStateInterface $form_state) {

  // Generate a list of possible parents
  // (not including this link or descendants).
  // @todo This must be handled in a #process handler.
  $term = $form_state
    ->getFormObject()
    ->getEntity();
  $defaults = taxonomy_menu_ui_get_menu_link_defaults($term);

  /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */
  $vid = $term
    ->bundle();
  $vocabulary = Vocabulary::load($vid);

  /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
  $menu_parent_selector = \Drupal::service('menu.parent_form_selector');
  $menu_names = menu_ui_get_menus();
  $vocabulary_menus = $vocabulary
    ->getThirdPartySetting('menu_ui', 'available_menus', [
    'main',
  ]);
  $available_menus = [];
  foreach ($vocabulary_menus as $menu) {
    $available_menus[$menu] = $menu_names[$menu];
  }
  if ($defaults['id']) {
    $default = $defaults['menu_name'] . ':' . $defaults['parent'];
  }
  else {
    $default = $vocabulary
      ->getThirdPartySetting('menu_ui', 'parent', 'main:');
  }
  $parent_element = $menu_parent_selector
    ->parentSelectElement($default, $defaults['id'], $available_menus);

  // If no possible parent menu items were found, there is nothing to display.
  if (empty($parent_element)) {
    return;
  }
  $current_user = \Drupal::currentUser();
  $access = $current_user
    ->hasPermission('administer menu');

  // Menu admin per menu integration.
  if (!$access && \Drupal::moduleHandler()
    ->moduleExists('menu_admin_per_menu')) {
    foreach (array_keys($available_menus) as $available_menu_id) {
      if ($access = $current_user
        ->hasPermission('administer ' . $available_menu_id . ' menu items')) {
        break;
      }
    }
  }
  $form['menu'] = [
    '#type' => 'details',
    '#title' => t('Menu settings'),
    '#access' => $access,
    '#open' => (bool) $defaults['id'],
    '#group' => 'advanced',
    '#attached' => [
      'library' => [
        'menu_ui/drupal.menu_ui',
      ],
    ],
    '#tree' => TRUE,
    '#weight' => 10,
    '#attributes' => [
      'class' => [
        'menu-link-form',
      ],
    ],
  ];
  $form['menu']['enabled'] = [
    '#type' => 'checkbox',
    '#title' => t('Provide a menu link'),
    '#default_value' => (int) (bool) $defaults['id'],
  ];
  $form['menu']['link'] = [
    '#type' => 'container',
    '#parents' => [
      'menu',
    ],
    '#states' => [
      'invisible' => [
        'input[name="menu[enabled]"]' => [
          'checked' => FALSE,
        ],
      ],
    ],
  ];

  // Populate the element with the link data.
  foreach ([
    'id',
    'entity_id',
  ] as $key) {
    $form['menu']['link'][$key] = [
      '#type' => 'value',
      '#value' => $defaults[$key],
    ];
  }
  $form['menu']['link']['title'] = [
    '#type' => 'textfield',
    '#title' => t('Menu link title'),
    '#default_value' => $defaults['title'],
    '#maxlength' => $defaults['title_max_length'],
  ];
  $form['menu']['link']['description'] = [
    '#type' => 'textarea',
    '#title' => t('Description'),
    '#default_value' => $defaults['description'],
    '#rows' => 1,
    '#description' => t('Shown when hovering over the menu link.'),
  ];
  $form['menu']['link']['menu_parent'] = $parent_element;
  $form['menu']['link']['menu_parent']['#title'] = t('Parent item');
  $form['menu']['link']['menu_parent']['#attributes']['class'][] = 'menu-parent-select';
  $form['menu']['link']['weight'] = [
    '#type' => 'number',
    '#title' => t('Weight'),
    '#default_value' => $defaults['weight'],
    '#description' => t('Menu links with lower weights are displayed before links with higher weights.'),
  ];
  foreach (array_keys($form['actions']) as $action) {
    if ($action != 'preview' && isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] === 'submit') {
      $form['actions'][$action]['#submit'][] = 'taxonomy_menu_ui_form_taxonomy_term_form_submit';
    }
  }
  $form['#entity_builders'][] = 'taxonomy_menu_ui_taxonomy_term_builder';
}

/**
 * Entity form builder to add the menu information to the taxonomy term.
 */
function taxonomy_menu_ui_taxonomy_term_builder($entity_type, TermInterface $entity, &$form, FormStateInterface $form_state) {
  $entity->menu = $form_state
    ->getValue('menu');
}

/**
 * Form submission handler for menu item field on the texonomy term form.
 *
 * @see taxonomy_menu_ui_form_taxonomy_term_form_alter()
 */
function taxonomy_menu_ui_form_taxonomy_term_form_submit($form, FormStateInterface $form_state) {
  $term = $form_state
    ->getFormObject()
    ->getEntity();
  if (!$form_state
    ->isValueEmpty('menu')) {
    $values = $form_state
      ->getValue('menu');
    if (empty($values['enabled'])) {
      if ($values['entity_id']) {
        $entity = MenuLinkContent::load($values['entity_id']);
        $entity
          ->delete();
      }
    }
    elseif (trim($values['title'])) {

      // Decompose the selected menu parent option into 'menu_name' and
      // 'parent', if the form used the default parent selection widget.
      if (!empty($values['menu_parent'])) {
        list($menu_name, $parent) = explode(':', $values['menu_parent'], 2);
        $values['menu_name'] = $menu_name;
        $values['parent'] = $parent;
      }
      _menu_ui_taxonomy_term_save($term, $values);
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter() for \Drupal\taxonomy\VocabularyForm.
 *
 * Adds menu options to the taxonomy vocabulary form.
 *
 * @see VocabularyForm::form()
 * @see taxonomy_menu_ui_form_taxonomy_vocabulary_form_submit()
 */
function taxonomy_menu_ui_form_taxonomy_vocabulary_form_alter(&$form, FormStateInterface $form_state) {

  /** @var \Drupal\Core\Menu\MenuParentFormSelectorInterface $menu_parent_selector */
  $menu_parent_selector = \Drupal::service('menu.parent_form_selector');
  $menu_options = menu_ui_get_menus();

  /** @var \Drupal\taxonomy\VocabularyInterface $vocabulary */
  $vocabulary = $form_state
    ->getFormObject()
    ->getEntity();
  $form['menu'] = [
    '#type' => 'details',
    '#title' => t('Menu settings'),
    '#attached' => [
      'library' => [
        'menu_ui/drupal.menu_ui.admin',
      ],
    ],
    '#group' => 'additional_settings',
  ];
  $form['menu']['menu_options'] = [
    '#type' => 'checkboxes',
    '#title' => t('Available menus'),
    '#default_value' => $vocabulary
      ->getThirdPartySetting('menu_ui', 'available_menus', [
      'main',
    ]),
    '#options' => $menu_options,
    '#description' => t('The menus available to place links in for this content type.'),
  ];

  // @todo See if we can avoid pre-loading all options by changing the form or
  //   using a #process callback. https://www.drupal.org/node/2310319
  //   To avoid an 'illegal option' error after saving the form we have to load
  //   all available menu parents. Otherwise, it is not possible to dynamically
  //   add options to the list using ajax.
  $options_cacheability = new CacheableMetadata();
  $options = $menu_parent_selector
    ->getParentSelectOptions('', NULL, $options_cacheability);
  $form['menu']['menu_parent'] = [
    '#type' => 'select',
    '#title' => t('Default parent item'),
    '#default_value' => $vocabulary
      ->getThirdPartySetting('menu_ui', 'parent', 'main:'),
    '#options' => $options,
    '#description' => t('Choose the menu item to be the default parent for a new link in the content authoring form.'),
    '#attributes' => [
      'class' => [
        'menu-title-select',
      ],
    ],
  ];
  $options_cacheability
    ->applyTo($form['menu']['menu_parent']);
  $form['#validate'][] = 'taxonomy_menu_ui_form_taxonomy_vocabulary_form_validate';
  $form['#entity_builders'][] = 'taxonomy_menu_ui_form_taxonomy_vocabulary_form_builder';
}

/**
 * Validate handler for forms with menu options.
 *
 * @see taxonomy_menu_ui_form_taxonomy_vocabulary_form_alter()
 */
function taxonomy_menu_ui_form_taxonomy_vocabulary_form_validate(&$form, FormStateInterface $form_state) {
  $available_menus = array_filter($form_state
    ->getValue('menu_options'));

  // If there is at least one menu allowed, the selected item should be in
  // one of them.
  if (count($available_menus)) {
    $menu_item_id_parts = explode(':', $form_state
      ->getValue('menu_parent'));
    if (!in_array($menu_item_id_parts[0], $available_menus)) {
      $form_state
        ->setErrorByName('menu_parent', t('The selected menu item is not under one of the selected menus.'));
    }
  }
  else {
    $form_state
      ->setValue('menu_parent', '');
  }
}

/**
 * Entity builder for the taxonomy vocabulary form with menu options.
 *
 * @see taxonomy_menu_ui_form_taxonomy_vocabulary_form_alter()
 */
function taxonomy_menu_ui_form_taxonomy_vocabulary_form_builder($entity_type, VocabularyInterface $vocabulary, &$form, FormStateInterface $form_state) {
  $vocabulary
    ->setThirdPartySetting('menu_ui', 'available_menus', array_values(array_filter($form_state
    ->getValue('menu_options'))));
  $vocabulary
    ->setThirdPartySetting('menu_ui', 'parent', $form_state
    ->getValue('menu_parent'));
}

/**
 * Implements hook_entity_extra_field_info().
 *
 * Add extra fields for each taxonomy vocabulary to show Menu settings.
 */
function taxonomy_menu_ui_entity_extra_field_info() {
  $extra = [];

  /** @var \Drupal\Core\Entity\EntityInterface $bundle */
  foreach (Vocabulary::loadMultiple() as $bundle) {
    $extra['taxonomy_term'][$bundle
      ->id()]['form']['menu'] = [
      'label' => t('Menu settings'),
      'description' => t('Field for menu settings'),
      'weight' => 10,
    ];
  }
  return $extra;
}

/**
 * Implements hook_ENTITY_TYPE_translation_delete().
 *
 * Remove translation from the menu.
 */
function taxonomy_menu_ui_taxonomy_term_translation_delete(EntityInterface $entity) {
  $language_id = $entity
    ->language()
    ->getId();
  $menus = \Drupal::entityTypeManager()
    ->getStorage('menu_link_content')
    ->loadByProperties([
    'link__uri' => 'internal:/taxonomy/term/' . $entity
      ->id(),
  ]);
  foreach ($menus as $menu) {
    if ($menu
      ->hasTranslation($language_id)) {
      $menu
        ->removeTranslation($language_id);
      $menu
        ->save();
    }
  }
}

Functions

Namesort descending Description
taxonomy_menu_ui_entity_extra_field_info Implements hook_entity_extra_field_info().
taxonomy_menu_ui_form_taxonomy_term_form_alter Implements hook_form_BASE_FORM_ID_alter() for \Drupal\taxonomy\TermForm.
taxonomy_menu_ui_form_taxonomy_term_form_submit Form submission handler for menu item field on the texonomy term form.
taxonomy_menu_ui_form_taxonomy_vocabulary_form_alter Implements hook_form_FORM_ID_alter() for \Drupal\taxonomy\VocabularyForm.
taxonomy_menu_ui_form_taxonomy_vocabulary_form_builder Entity builder for the taxonomy vocabulary form with menu options.
taxonomy_menu_ui_form_taxonomy_vocabulary_form_validate Validate handler for forms with menu options.
taxonomy_menu_ui_get_menu_link_defaults Returns the definition for a menu link for the given term.
taxonomy_menu_ui_taxonomy_term_builder Entity form builder to add the menu information to the taxonomy term.
taxonomy_menu_ui_taxonomy_term_translation_delete Implements hook_ENTITY_TYPE_translation_delete().
_menu_ui_taxonomy_term_save Helper function to create or update a menu link for a taxonomy term.