getActive('menu_link_content', $values['entity_id']); if ($entity->isTranslatable() && $node->isTranslatable()) { if (!$entity->hasTranslation($node->language()->getId())) { $entity = $entity->addTranslation($node->language()->getId(), $entity->toArray()); } else { $entity = $entity->getTranslation($node->language()->getId()); } } else { // Ensure the entity matches the node language. $entity = $entity->getUntranslated(); $entity->set($entity->getEntityType()->getKey('langcode'), $node->language()->getId()); } } else { // Create a new menu_link_content entity. $entity = MenuLinkContent::create([ 'link' => ['uri' => 'entity:node/' . $node->id()], 'langcode' => $node->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 = $values['weight'] ?? 0; if ($entity->isNew()) { // @todo The menu link doesn't need to be changed in a workspace context. // Fix this in https://www.drupal.org/project/drupal/issues/3511204. if (!$node->isDefaultRevision() && $node->hasLinkTemplate('latest-version')) { // If a new menu link is created while saving the node as a pending draft // (non-default revision), store it as a link to the latest version. // That ensures that there is a regular, valid link target that is // only visible to users with permission to view the latest version. $entity->get('link')->uri = 'internal:/node/' . $node->id() . '/latest'; } } else { $entity->isDefaultRevision($node->isDefaultRevision()); if (!$entity->isDefaultRevision()) { $entity->setNewRevision(TRUE); } elseif ($entity->get('link')->uri !== 'entity:node/' . $node->id()) { $entity->get('link')->uri = 'entity:node/' . $node->id(); } } $entity->save(); } /** * Returns the definition for a menu link for the given node. * * @param \Drupal\node\NodeInterface $node * The node entity. * * @return array * An array that contains default values for the menu link form. */ function menu_ui_get_menu_link_defaults(NodeInterface $node) { // Prepare the definition for the edit form. /** @var \Drupal\node\NodeTypeInterface $node_type */ $node_type = $node->type->entity; $menu_name = strtok($node_type->getThirdPartySetting('menu_ui', 'parent', 'main:'), ':'); $defaults = FALSE; if ($node->id()) { $id = FALSE; // Give priority to the default menu. $type_menus = $node_type->getThirdPartySetting('menu_ui', 'available_menus', ['main']); // An existing menu link either points to the canonical or the latest path, // in case of a new menu link that was creating while saving as a pending // draft. $uri_candidates = ['entity:node/' . $node->id(), 'internal:/node/' . $node->id() . '/latest']; if (in_array($menu_name, $type_menus)) { $query = \Drupal::entityQuery('menu_link_content') ->accessCheck(TRUE) ->condition('link.uri', $uri_candidates, 'IN') ->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($type_menus)) { $query = \Drupal::entityQuery('menu_link_content') ->accessCheck(TRUE) ->condition('link.uri', $uri_candidates, 'IN') ->condition('menu_name', array_values($type_menus), 'IN') ->sort('id', 'ASC') ->range(0, 1); $result = $query->execute(); $id = (!empty($result)) ? reset($result) : FALSE; } if ($id) { $menu_link = \Drupal::service('entity.repository')->getActive('menu_link_content', $id); $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(), 'description_max_length' => $menu_link->getFieldDefinitions()['description']->getSetting('max_length'), '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'); $description_max_length = $field_definitions['description']->getSetting('max_length'); $defaults = [ 'entity_id' => 0, 'id' => '', 'title' => '', 'title_max_length' => $max_length, 'description' => '', 'description_max_length' => $description_max_length, 'menu_name' => $menu_name, 'parent' => '', 'weight' => 0, ]; } return $defaults; } /** * Entity form builder to add the menu information to the node. */ function menu_ui_node_builder($entity_type, NodeInterface $entity, &$form, FormStateInterface $form_state): void { $entity->menu = $form_state->getValue('menu'); } /** * Form submission handler for menu item field on the node form. * * @see menu_ui_form_node_form_alter() */ function menu_ui_form_node_form_submit($form, FormStateInterface $form_state): void { $node = $form_state->getFormObject()->getEntity(); if (!$form_state->isValueEmpty('menu')) { $values = $form_state->getValue('menu'); if (empty($values['enabled'])) { if ($values['entity_id']) { $entity = \Drupal::service('entity.repository')->getActive('menu_link_content', $values['entity_id']); $entity->delete(); } } else { // In case the menu title was left empty, fall back to the node title. if (empty(trim($values['title']))) { $values['title'] = $node->label(); } // 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'])) { [$menu_name, $parent] = explode(':', $values['menu_parent'], 2); $values['menu_name'] = $menu_name; $values['parent'] = $parent; } _menu_ui_node_save($node, $values); } } } /** * Validate handler for forms with menu options. * * @see menu_ui_form_node_type_form_alter() */ function menu_ui_form_node_type_form_validate(&$form, FormStateInterface $form_state): void { $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 link is not under one of the selected menus.')); } } else { $form_state->setValue('menu_parent', ''); } } /** * Entity builder for the node type form with menu options. * * @see menu_ui_form_node_type_form_alter() */ function menu_ui_form_node_type_form_builder($entity_type, NodeTypeInterface $type, &$form, FormStateInterface $form_state): void { $type->setThirdPartySetting('menu_ui', 'available_menus', array_values(array_filter($form_state->getValue('menu_options')))); $type->setThirdPartySetting('menu_ui', 'parent', $form_state->getValue('menu_parent')); } /** * Implements hook_preprocess_HOOK() for block templates. */ function menu_ui_preprocess_block(&$variables): void { if ($variables['configuration']['provider'] == 'menu_ui') { $variables['attributes']['role'] = 'navigation'; } }