Chris@0: TRUE]; Chris@0: Chris@0: /** Chris@0: * Constructs a MenuForm object. Chris@0: * Chris@0: * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager Chris@0: * The menu link manager. Chris@0: * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree Chris@0: * The menu tree service. Chris@0: * @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator Chris@0: * The link generator. Chris@18: * @param \Drupal\menu_link_content\MenuLinkContentStorageInterface $menu_link_content_storage Chris@18: * The menu link content storage handler. Chris@0: */ Chris@18: public function __construct(MenuLinkManagerInterface $menu_link_manager, MenuLinkTreeInterface $menu_tree, LinkGeneratorInterface $link_generator, MenuLinkContentStorageInterface $menu_link_content_storage) { Chris@0: $this->menuLinkManager = $menu_link_manager; Chris@0: $this->menuTree = $menu_tree; Chris@0: $this->linkGenerator = $link_generator; Chris@18: $this->menuLinkContentStorage = $menu_link_content_storage; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function create(ContainerInterface $container) { Chris@0: return new static( Chris@0: $container->get('plugin.manager.menu.link'), Chris@0: $container->get('menu.link_tree'), Chris@18: $container->get('link_generator'), Chris@18: $container->get('entity_type.manager')->getStorage('menu_link_content') Chris@0: ); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function form(array $form, FormStateInterface $form_state) { Chris@0: $menu = $this->entity; Chris@0: Chris@0: if ($this->operation == 'edit') { Chris@0: $form['#title'] = $this->t('Edit menu %label', ['%label' => $menu->label()]); Chris@0: } Chris@0: Chris@0: $form['label'] = [ Chris@0: '#type' => 'textfield', Chris@0: '#title' => $this->t('Title'), Chris@0: '#default_value' => $menu->label(), Chris@0: '#required' => TRUE, Chris@0: ]; Chris@0: $form['id'] = [ Chris@0: '#type' => 'machine_name', Chris@0: '#title' => $this->t('Menu name'), Chris@0: '#default_value' => $menu->id(), Chris@0: '#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI, Chris@0: '#description' => $this->t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'), Chris@0: '#machine_name' => [ Chris@0: 'exists' => [$this, 'menuNameExists'], Chris@0: 'source' => ['label'], Chris@0: 'replace_pattern' => '[^a-z0-9-]+', Chris@0: 'replace' => '-', Chris@0: ], Chris@0: // A menu's machine name cannot be changed. Chris@0: '#disabled' => !$menu->isNew() || $menu->isLocked(), Chris@0: ]; Chris@0: $form['description'] = [ Chris@0: '#type' => 'textfield', Chris@0: '#title' => t('Administrative summary'), Chris@0: '#maxlength' => 512, Chris@0: '#default_value' => $menu->getDescription(), Chris@0: ]; Chris@0: Chris@0: $form['langcode'] = [ Chris@0: '#type' => 'language_select', Chris@0: '#title' => t('Menu language'), Chris@0: '#languages' => LanguageInterface::STATE_ALL, Chris@0: '#default_value' => $menu->language()->getId(), Chris@0: ]; Chris@0: Chris@0: // Add menu links administration form for existing menus. Chris@0: if (!$menu->isNew() || $menu->isLocked()) { Chris@0: // Form API supports constructing and validating self-contained sections Chris@0: // within forms, but does not allow handling the form section's submission Chris@0: // equally separated yet. Therefore, we use a $form_state key to point to Chris@0: // the parents of the form section. Chris@0: // @see self::submitOverviewForm() Chris@0: $form_state->set('menu_overview_form_parents', ['links']); Chris@0: $form['links'] = []; Chris@0: $form['links'] = $this->buildOverviewForm($form['links'], $form_state); Chris@0: } Chris@0: Chris@0: return parent::form($form, $form_state); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns whether a menu name already exists. Chris@0: * Chris@0: * @param string $value Chris@0: * The name of the menu. Chris@0: * Chris@0: * @return bool Chris@0: * Returns TRUE if the menu already exists, FALSE otherwise. Chris@0: */ Chris@0: public function menuNameExists($value) { Chris@0: // Check first to see if a menu with this ID exists. Chris@0: if ($this->entityTypeManager->getStorage('menu')->getQuery()->condition('id', $value)->range(0, 1)->count()->execute()) { Chris@0: return TRUE; Chris@0: } Chris@0: Chris@0: // Check for a link assigned to this menu. Chris@0: return $this->menuLinkManager->menuNameInUse($value); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function save(array $form, FormStateInterface $form_state) { Chris@0: $menu = $this->entity; Chris@0: $status = $menu->save(); Chris@18: $edit_link = $this->entity->toLink($this->t('Edit'), 'edit-form')->toString(); Chris@0: if ($status == SAVED_UPDATED) { Chris@17: $this->messenger()->addStatus($this->t('Menu %label has been updated.', ['%label' => $menu->label()])); Chris@0: $this->logger('menu')->notice('Menu %label has been updated.', ['%label' => $menu->label(), 'link' => $edit_link]); Chris@0: } Chris@0: else { Chris@17: $this->messenger()->addStatus($this->t('Menu %label has been added.', ['%label' => $menu->label()])); Chris@0: $this->logger('menu')->notice('Menu %label has been added.', ['%label' => $menu->label(), 'link' => $edit_link]); Chris@0: } Chris@0: Chris@18: $form_state->setRedirectUrl($this->entity->toUrl('edit-form')); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function submitForm(array &$form, FormStateInterface $form_state) { Chris@0: parent::submitForm($form, $form_state); Chris@0: Chris@0: if (!$this->entity->isNew() || $this->entity->isLocked()) { Chris@0: $this->submitOverviewForm($form, $form_state); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Form constructor to edit an entire menu tree at once. Chris@0: * Chris@0: * Shows for one menu the menu links accessible to the current user and Chris@0: * relevant operations. Chris@0: * Chris@0: * This form constructor can be integrated as a section into another form. It Chris@0: * relies on the following keys in $form_state: Chris@0: * - menu: A menu entity. Chris@0: * - menu_overview_form_parents: An array containing the parent keys to this Chris@0: * form. Chris@0: * Forms integrating this section should call menu_overview_form_submit() from Chris@0: * their form submit handler. Chris@0: */ Chris@0: protected function buildOverviewForm(array &$form, FormStateInterface $form_state) { Chris@0: // Ensure that menu_overview_form_submit() knows the parents of this form Chris@0: // section. Chris@0: if (!$form_state->has('menu_overview_form_parents')) { Chris@0: $form_state->set('menu_overview_form_parents', []); Chris@0: } Chris@0: Chris@0: $form['#attached']['library'][] = 'menu_ui/drupal.menu_ui.adminforms'; Chris@0: Chris@0: $tree = $this->menuTree->load($this->entity->id(), new MenuTreeParameters()); Chris@0: Chris@0: // We indicate that a menu administrator is running the menu access check. Chris@0: $this->getRequest()->attributes->set('_menu_admin', TRUE); Chris@0: $manipulators = [ Chris@0: ['callable' => 'menu.default_tree_manipulators:checkAccess'], Chris@0: ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'], Chris@0: ]; Chris@0: $tree = $this->menuTree->transform($tree, $manipulators); Chris@0: $this->getRequest()->attributes->set('_menu_admin', FALSE); Chris@0: Chris@0: // Determine the delta; the number of weights to be made available. Chris@0: $count = function (array $tree) { Chris@0: $sum = function ($carry, MenuLinkTreeElement $item) { Chris@0: return $carry + $item->count(); Chris@0: }; Chris@0: return array_reduce($tree, $sum); Chris@0: }; Chris@0: $delta = max($count($tree), 50); Chris@0: Chris@0: $form['links'] = [ Chris@0: '#type' => 'table', Chris@0: '#theme' => 'table__menu_overview', Chris@0: '#header' => [ Chris@0: $this->t('Menu link'), Chris@0: [ Chris@0: 'data' => $this->t('Enabled'), Chris@0: 'class' => ['checkbox'], Chris@0: ], Chris@0: $this->t('Weight'), Chris@0: [ Chris@0: 'data' => $this->t('Operations'), Chris@0: 'colspan' => 3, Chris@0: ], Chris@0: ], Chris@0: '#attributes' => [ Chris@0: 'id' => 'menu-overview', Chris@0: ], Chris@0: '#tabledrag' => [ Chris@0: [ Chris@0: 'action' => 'match', Chris@0: 'relationship' => 'parent', Chris@0: 'group' => 'menu-parent', Chris@0: 'subgroup' => 'menu-parent', Chris@0: 'source' => 'menu-id', Chris@0: 'hidden' => TRUE, Chris@0: 'limit' => \Drupal::menuTree()->maxDepth() - 1, Chris@0: ], Chris@0: [ Chris@0: 'action' => 'order', Chris@0: 'relationship' => 'sibling', Chris@0: 'group' => 'menu-weight', Chris@0: ], Chris@0: ], Chris@0: ]; Chris@0: Chris@0: $form['links']['#empty'] = $this->t('There are no menu links yet. Add link.', [ Chris@0: ':url' => $this->url('entity.menu.add_link_form', ['menu' => $this->entity->id()], [ Chris@18: 'query' => ['destination' => $this->entity->toUrl('edit-form')->toString()], Chris@0: ]), Chris@0: ]); Chris@0: $links = $this->buildOverviewTreeForm($tree, $delta); Chris@18: Chris@18: // Get the menu links which have pending revisions, and disable the Chris@18: // tabledrag if there are any. Chris@18: $edited_ids = array_filter(array_map(function ($element) { Chris@18: return is_array($element) && isset($element['#item']) && $element['#item']->link instanceof MenuLinkContent ? $element['#item']->link->getMetaData()['entity_id'] : NULL; Chris@18: }, $links)); Chris@18: $pending_menu_link_ids = array_intersect($this->menuLinkContentStorage->getMenuLinkIdsWithPendingRevisions(), $edited_ids); Chris@18: if ($pending_menu_link_ids) { Chris@18: $form['help'] = [ Chris@18: '#type' => 'container', Chris@18: 'message' => [ Chris@18: '#markup' => $this->formatPlural( Chris@18: count($pending_menu_link_ids), Chris@18: '%capital_name contains 1 menu link with pending revisions. Manipulation of a menu tree having links with pending revisions is not supported, but you can re-enable manipulation by getting each menu link to a published state.', Chris@18: '%capital_name contains @count menu links with pending revisions. Manipulation of a menu tree having links with pending revisions is not supported, but you can re-enable manipulation by getting each menu link to a published state.', Chris@18: [ Chris@18: '%capital_name' => $this->entity->label(), Chris@18: ] Chris@18: ), Chris@18: ], Chris@18: '#attributes' => ['class' => ['messages', 'messages--warning']], Chris@18: '#weight' => -10, Chris@18: ]; Chris@18: Chris@18: unset($form['links']['#tabledrag']); Chris@18: unset($form['links']['#header'][2]); Chris@18: } Chris@18: Chris@0: foreach (Element::children($links) as $id) { Chris@0: if (isset($links[$id]['#item'])) { Chris@0: $element = $links[$id]; Chris@0: Chris@18: $is_pending_menu_link = isset($element['#item']->link->getMetaData()['entity_id']) Chris@18: && in_array($element['#item']->link->getMetaData()['entity_id'], $pending_menu_link_ids); Chris@18: Chris@0: $form['links'][$id]['#item'] = $element['#item']; Chris@0: Chris@0: // TableDrag: Mark the table row as draggable. Chris@0: $form['links'][$id]['#attributes'] = $element['#attributes']; Chris@0: $form['links'][$id]['#attributes']['class'][] = 'draggable'; Chris@0: Chris@18: if ($is_pending_menu_link) { Chris@18: $form['links'][$id]['#attributes']['class'][] = 'color-warning'; Chris@18: $form['links'][$id]['#attributes']['class'][] = 'menu-link-content--pending-revision'; Chris@18: } Chris@18: Chris@0: // TableDrag: Sort the table row according to its existing/configured weight. Chris@0: $form['links'][$id]['#weight'] = $element['#item']->link->getWeight(); Chris@0: Chris@0: // Add special classes to be used for tabledrag.js. Chris@0: $element['parent']['#attributes']['class'] = ['menu-parent']; Chris@0: $element['weight']['#attributes']['class'] = ['menu-weight']; Chris@0: $element['id']['#attributes']['class'] = ['menu-id']; Chris@0: Chris@0: $form['links'][$id]['title'] = [ Chris@0: [ Chris@0: '#theme' => 'indentation', Chris@0: '#size' => $element['#item']->depth - 1, Chris@0: ], Chris@0: $element['title'], Chris@0: ]; Chris@0: $form['links'][$id]['enabled'] = $element['enabled']; Chris@0: $form['links'][$id]['enabled']['#wrapper_attributes']['class'] = ['checkbox', 'menu-enabled']; Chris@0: Chris@18: // Disallow changing the publishing status of a pending revision. Chris@18: if ($is_pending_menu_link) { Chris@18: $form['links'][$id]['enabled']['#access'] = FALSE; Chris@18: } Chris@18: Chris@18: if (!$pending_menu_link_ids) { Chris@18: $form['links'][$id]['weight'] = $element['weight']; Chris@18: } Chris@0: Chris@0: // Operations (dropbutton) column. Chris@0: $form['links'][$id]['operations'] = $element['operations']; Chris@0: Chris@0: $form['links'][$id]['id'] = $element['id']; Chris@0: $form['links'][$id]['parent'] = $element['parent']; Chris@0: } Chris@0: } Chris@0: Chris@0: return $form; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Recursive helper function for buildOverviewForm(). Chris@0: * Chris@0: * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree Chris@0: * The tree retrieved by \Drupal\Core\Menu\MenuLinkTreeInterface::load(). Chris@0: * @param int $delta Chris@0: * The default number of menu items used in the menu weight selector is 50. Chris@0: * Chris@0: * @return array Chris@0: * The overview tree form. Chris@0: */ Chris@0: protected function buildOverviewTreeForm($tree, $delta) { Chris@0: $form = &$this->overviewTreeForm; Chris@0: $tree_access_cacheability = new CacheableMetadata(); Chris@0: foreach ($tree as $element) { Chris@0: $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access)); Chris@0: Chris@0: // Only render accessible links. Chris@0: if (!$element->access->isAllowed()) { Chris@0: continue; Chris@0: } Chris@0: Chris@0: /** @var \Drupal\Core\Menu\MenuLinkInterface $link */ Chris@0: $link = $element->link; Chris@0: if ($link) { Chris@0: $id = 'menu_plugin_id:' . $link->getPluginId(); Chris@0: $form[$id]['#item'] = $element; Chris@0: $form[$id]['#attributes'] = $link->isEnabled() ? ['class' => ['menu-enabled']] : ['class' => ['menu-disabled']]; Chris@0: $form[$id]['title'] = Link::fromTextAndUrl($link->getTitle(), $link->getUrlObject())->toRenderable(); Chris@0: if (!$link->isEnabled()) { Chris@0: $form[$id]['title']['#suffix'] = ' (' . $this->t('disabled') . ')'; Chris@0: } Chris@0: // @todo Remove this in https://www.drupal.org/node/2568785. Chris@0: elseif ($id === 'menu_plugin_id:user.logout') { Chris@0: $form[$id]['title']['#suffix'] = ' (' . $this->t('Log in for anonymous users') . ')'; Chris@0: } Chris@0: // @todo Remove this in https://www.drupal.org/node/2568785. Chris@0: elseif (($url = $link->getUrlObject()) && $url->isRouted() && $url->getRouteName() == 'user.page') { Chris@0: $form[$id]['title']['#suffix'] = ' (' . $this->t('logged in users only') . ')'; Chris@0: } Chris@0: Chris@0: $form[$id]['enabled'] = [ Chris@0: '#type' => 'checkbox', Chris@0: '#title' => $this->t('Enable @title menu link', ['@title' => $link->getTitle()]), Chris@0: '#title_display' => 'invisible', Chris@0: '#default_value' => $link->isEnabled(), Chris@0: ]; Chris@0: $form[$id]['weight'] = [ Chris@0: '#type' => 'weight', Chris@0: '#delta' => $delta, Chris@0: '#default_value' => $link->getWeight(), Chris@0: '#title' => $this->t('Weight for @title', ['@title' => $link->getTitle()]), Chris@0: '#title_display' => 'invisible', Chris@0: ]; Chris@0: $form[$id]['id'] = [ Chris@0: '#type' => 'hidden', Chris@0: '#value' => $link->getPluginId(), Chris@0: ]; Chris@0: $form[$id]['parent'] = [ Chris@0: '#type' => 'hidden', Chris@0: '#default_value' => $link->getParent(), Chris@0: ]; Chris@0: // Build a list of operations. Chris@0: $operations = []; Chris@0: $operations['edit'] = [ Chris@0: 'title' => $this->t('Edit'), Chris@0: ]; Chris@0: // Allow for a custom edit link per plugin. Chris@0: $edit_route = $link->getEditRoute(); Chris@0: if ($edit_route) { Chris@0: $operations['edit']['url'] = $edit_route; Chris@0: // Bring the user back to the menu overview. Chris@0: $operations['edit']['query'] = $this->getDestinationArray(); Chris@0: } Chris@0: else { Chris@0: // Fall back to the standard edit link. Chris@0: $operations['edit'] += [ Chris@0: 'url' => Url::fromRoute('menu_ui.link_edit', ['menu_link_plugin' => $link->getPluginId()]), Chris@0: ]; Chris@0: } Chris@0: // Links can either be reset or deleted, not both. Chris@0: if ($link->isResettable()) { Chris@0: $operations['reset'] = [ Chris@0: 'title' => $this->t('Reset'), Chris@0: 'url' => Url::fromRoute('menu_ui.link_reset', ['menu_link_plugin' => $link->getPluginId()]), Chris@0: ]; Chris@0: } Chris@0: elseif ($delete_link = $link->getDeleteRoute()) { Chris@0: $operations['delete']['url'] = $delete_link; Chris@0: $operations['delete']['query'] = $this->getDestinationArray(); Chris@0: $operations['delete']['title'] = $this->t('Delete'); Chris@0: } Chris@0: if ($link->isTranslatable()) { Chris@0: $operations['translate'] = [ Chris@0: 'title' => $this->t('Translate'), Chris@0: 'url' => $link->getTranslateRoute(), Chris@0: ]; Chris@0: } Chris@0: $form[$id]['operations'] = [ Chris@0: '#type' => 'operations', Chris@0: '#links' => $operations, Chris@0: ]; Chris@0: } Chris@0: Chris@0: if ($element->subtree) { Chris@0: $this->buildOverviewTreeForm($element->subtree, $delta); Chris@0: } Chris@0: } Chris@0: Chris@0: $tree_access_cacheability Chris@0: ->merge(CacheableMetadata::createFromRenderArray($form)) Chris@0: ->applyTo($form); Chris@0: Chris@0: return $form; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Submit handler for the menu overview form. Chris@0: * Chris@0: * This function takes great care in saving parent items first, then items Chris@0: * underneath them. Saving items in the incorrect order can break the tree. Chris@0: */ Chris@0: protected function submitOverviewForm(array $complete_form, FormStateInterface $form_state) { Chris@0: // Form API supports constructing and validating self-contained sections Chris@0: // within forms, but does not allow to handle the form section's submission Chris@0: // equally separated yet. Therefore, we use a $form_state key to point to Chris@0: // the parents of the form section. Chris@0: $parents = $form_state->get('menu_overview_form_parents'); Chris@0: $input = NestedArray::getValue($form_state->getUserInput(), $parents); Chris@0: $form = &NestedArray::getValue($complete_form, $parents); Chris@0: Chris@0: // When dealing with saving menu items, the order in which these items are Chris@0: // saved is critical. If a changed child item is saved before its parent, Chris@0: // the child item could be saved with an invalid path past its immediate Chris@0: // parent. To prevent this, save items in the form in the same order they Chris@0: // are sent, ensuring parents are saved first, then their children. Chris@0: // See https://www.drupal.org/node/181126#comment-632270. Chris@0: $order = is_array($input) ? array_flip(array_keys($input)) : []; Chris@0: // Update our original form with the new order. Chris@0: $form = array_intersect_key(array_merge($order, $form), $form); Chris@0: Chris@0: $fields = ['weight', 'parent', 'enabled']; Chris@0: $form_links = $form['links']; Chris@0: foreach (Element::children($form_links) as $id) { Chris@0: if (isset($form_links[$id]['#item'])) { Chris@0: $element = $form_links[$id]; Chris@0: $updated_values = []; Chris@0: // Update any fields that have changed in this menu item. Chris@0: foreach ($fields as $field) { Chris@18: if (isset($element[$field]['#value']) && $element[$field]['#value'] != $element[$field]['#default_value']) { Chris@0: $updated_values[$field] = $element[$field]['#value']; Chris@0: } Chris@0: } Chris@0: if ($updated_values) { Chris@0: // Use the ID from the actual plugin instance since the hidden value Chris@0: // in the form could be tampered with. Chris@0: $this->menuLinkManager->updateDefinition($element['#item']->link->getPLuginId(), $updated_values); Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: }