3 namespace Drupal\menu_ui;
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Cache\CacheableMetadata;
7 use Drupal\Core\Entity\EntityForm;
8 use Drupal\Core\Form\FormStateInterface;
9 use Drupal\Core\Language\LanguageInterface;
11 use Drupal\Core\Menu\MenuLinkManagerInterface;
12 use Drupal\Core\Menu\MenuLinkTreeElement;
13 use Drupal\Core\Menu\MenuLinkTreeInterface;
14 use Drupal\Core\Menu\MenuTreeParameters;
15 use Drupal\Core\Render\Element;
17 use Drupal\Core\Utility\LinkGeneratorInterface;
18 use Symfony\Component\DependencyInjection\ContainerInterface;
21 * Base form for menu edit forms.
25 class MenuForm extends EntityForm {
28 * The menu link manager.
30 * @var \Drupal\Core\Menu\MenuLinkManagerInterface
32 protected $menuLinkManager;
35 * The menu tree service.
37 * @var \Drupal\Core\Menu\MenuLinkTreeInterface
44 * @var \Drupal\Core\Utility\LinkGeneratorInterface
46 protected $linkGenerator;
49 * The overview tree form.
53 protected $overviewTreeForm = ['#tree' => TRUE];
56 * Constructs a MenuForm object.
58 * @param \Drupal\Core\Menu\MenuLinkManagerInterface $menu_link_manager
59 * The menu link manager.
60 * @param \Drupal\Core\Menu\MenuLinkTreeInterface $menu_tree
61 * The menu tree service.
62 * @param \Drupal\Core\Utility\LinkGeneratorInterface $link_generator
65 public function __construct(MenuLinkManagerInterface $menu_link_manager, MenuLinkTreeInterface $menu_tree, LinkGeneratorInterface $link_generator) {
66 $this->menuLinkManager = $menu_link_manager;
67 $this->menuTree = $menu_tree;
68 $this->linkGenerator = $link_generator;
74 public static function create(ContainerInterface $container) {
76 $container->get('plugin.manager.menu.link'),
77 $container->get('menu.link_tree'),
78 $container->get('link_generator')
85 public function form(array $form, FormStateInterface $form_state) {
86 $menu = $this->entity;
88 if ($this->operation == 'edit') {
89 $form['#title'] = $this->t('Edit menu %label', ['%label' => $menu->label()]);
93 '#type' => 'textfield',
94 '#title' => $this->t('Title'),
95 '#default_value' => $menu->label(),
99 '#type' => 'machine_name',
100 '#title' => $this->t('Menu name'),
101 '#default_value' => $menu->id(),
102 '#maxlength' => MENU_MAX_MENU_NAME_LENGTH_UI,
103 '#description' => $this->t('A unique name to construct the URL for the menu. It must only contain lowercase letters, numbers and hyphens.'),
105 'exists' => [$this, 'menuNameExists'],
106 'source' => ['label'],
107 'replace_pattern' => '[^a-z0-9-]+',
110 // A menu's machine name cannot be changed.
111 '#disabled' => !$menu->isNew() || $menu->isLocked(),
113 $form['description'] = [
114 '#type' => 'textfield',
115 '#title' => t('Administrative summary'),
117 '#default_value' => $menu->getDescription(),
120 $form['langcode'] = [
121 '#type' => 'language_select',
122 '#title' => t('Menu language'),
123 '#languages' => LanguageInterface::STATE_ALL,
124 '#default_value' => $menu->language()->getId(),
127 // Add menu links administration form for existing menus.
128 if (!$menu->isNew() || $menu->isLocked()) {
129 // Form API supports constructing and validating self-contained sections
130 // within forms, but does not allow handling the form section's submission
131 // equally separated yet. Therefore, we use a $form_state key to point to
132 // the parents of the form section.
133 // @see self::submitOverviewForm()
134 $form_state->set('menu_overview_form_parents', ['links']);
136 $form['links'] = $this->buildOverviewForm($form['links'], $form_state);
139 return parent::form($form, $form_state);
143 * Returns whether a menu name already exists.
145 * @param string $value
146 * The name of the menu.
149 * Returns TRUE if the menu already exists, FALSE otherwise.
151 public function menuNameExists($value) {
152 // Check first to see if a menu with this ID exists.
153 if ($this->entityTypeManager->getStorage('menu')->getQuery()->condition('id', $value)->range(0, 1)->count()->execute()) {
157 // Check for a link assigned to this menu.
158 return $this->menuLinkManager->menuNameInUse($value);
164 public function save(array $form, FormStateInterface $form_state) {
165 $menu = $this->entity;
166 $status = $menu->save();
167 $edit_link = $this->entity->link($this->t('Edit'));
168 if ($status == SAVED_UPDATED) {
169 $this->messenger()->addStatus($this->t('Menu %label has been updated.', ['%label' => $menu->label()]));
170 $this->logger('menu')->notice('Menu %label has been updated.', ['%label' => $menu->label(), 'link' => $edit_link]);
173 $this->messenger()->addStatus($this->t('Menu %label has been added.', ['%label' => $menu->label()]));
174 $this->logger('menu')->notice('Menu %label has been added.', ['%label' => $menu->label(), 'link' => $edit_link]);
177 $form_state->setRedirectUrl($this->entity->urlInfo('edit-form'));
183 public function submitForm(array &$form, FormStateInterface $form_state) {
184 parent::submitForm($form, $form_state);
186 if (!$this->entity->isNew() || $this->entity->isLocked()) {
187 $this->submitOverviewForm($form, $form_state);
192 * Form constructor to edit an entire menu tree at once.
194 * Shows for one menu the menu links accessible to the current user and
195 * relevant operations.
197 * This form constructor can be integrated as a section into another form. It
198 * relies on the following keys in $form_state:
199 * - menu: A menu entity.
200 * - menu_overview_form_parents: An array containing the parent keys to this
202 * Forms integrating this section should call menu_overview_form_submit() from
203 * their form submit handler.
205 protected function buildOverviewForm(array &$form, FormStateInterface $form_state) {
206 // Ensure that menu_overview_form_submit() knows the parents of this form
208 if (!$form_state->has('menu_overview_form_parents')) {
209 $form_state->set('menu_overview_form_parents', []);
212 $form['#attached']['library'][] = 'menu_ui/drupal.menu_ui.adminforms';
214 $tree = $this->menuTree->load($this->entity->id(), new MenuTreeParameters());
216 // We indicate that a menu administrator is running the menu access check.
217 $this->getRequest()->attributes->set('_menu_admin', TRUE);
219 ['callable' => 'menu.default_tree_manipulators:checkAccess'],
220 ['callable' => 'menu.default_tree_manipulators:generateIndexAndSort'],
222 $tree = $this->menuTree->transform($tree, $manipulators);
223 $this->getRequest()->attributes->set('_menu_admin', FALSE);
225 // Determine the delta; the number of weights to be made available.
226 $count = function (array $tree) {
227 $sum = function ($carry, MenuLinkTreeElement $item) {
228 return $carry + $item->count();
230 return array_reduce($tree, $sum);
232 $delta = max($count($tree), 50);
236 '#theme' => 'table__menu_overview',
238 $this->t('Menu link'),
240 'data' => $this->t('Enabled'),
241 'class' => ['checkbox'],
245 'data' => $this->t('Operations'),
250 'id' => 'menu-overview',
255 'relationship' => 'parent',
256 'group' => 'menu-parent',
257 'subgroup' => 'menu-parent',
258 'source' => 'menu-id',
260 'limit' => \Drupal::menuTree()->maxDepth() - 1,
264 'relationship' => 'sibling',
265 'group' => 'menu-weight',
270 $form['links']['#empty'] = $this->t('There are no menu links yet. <a href=":url">Add link</a>.', [
271 ':url' => $this->url('entity.menu.add_link_form', ['menu' => $this->entity->id()], [
272 'query' => ['destination' => $this->entity->url('edit-form')],
275 $links = $this->buildOverviewTreeForm($tree, $delta);
276 foreach (Element::children($links) as $id) {
277 if (isset($links[$id]['#item'])) {
278 $element = $links[$id];
280 $form['links'][$id]['#item'] = $element['#item'];
282 // TableDrag: Mark the table row as draggable.
283 $form['links'][$id]['#attributes'] = $element['#attributes'];
284 $form['links'][$id]['#attributes']['class'][] = 'draggable';
286 // TableDrag: Sort the table row according to its existing/configured weight.
287 $form['links'][$id]['#weight'] = $element['#item']->link->getWeight();
289 // Add special classes to be used for tabledrag.js.
290 $element['parent']['#attributes']['class'] = ['menu-parent'];
291 $element['weight']['#attributes']['class'] = ['menu-weight'];
292 $element['id']['#attributes']['class'] = ['menu-id'];
294 $form['links'][$id]['title'] = [
296 '#theme' => 'indentation',
297 '#size' => $element['#item']->depth - 1,
301 $form['links'][$id]['enabled'] = $element['enabled'];
302 $form['links'][$id]['enabled']['#wrapper_attributes']['class'] = ['checkbox', 'menu-enabled'];
304 $form['links'][$id]['weight'] = $element['weight'];
306 // Operations (dropbutton) column.
307 $form['links'][$id]['operations'] = $element['operations'];
309 $form['links'][$id]['id'] = $element['id'];
310 $form['links'][$id]['parent'] = $element['parent'];
318 * Recursive helper function for buildOverviewForm().
320 * @param \Drupal\Core\Menu\MenuLinkTreeElement[] $tree
321 * The tree retrieved by \Drupal\Core\Menu\MenuLinkTreeInterface::load().
323 * The default number of menu items used in the menu weight selector is 50.
326 * The overview tree form.
328 protected function buildOverviewTreeForm($tree, $delta) {
329 $form = &$this->overviewTreeForm;
330 $tree_access_cacheability = new CacheableMetadata();
331 foreach ($tree as $element) {
332 $tree_access_cacheability = $tree_access_cacheability->merge(CacheableMetadata::createFromObject($element->access));
334 // Only render accessible links.
335 if (!$element->access->isAllowed()) {
339 /** @var \Drupal\Core\Menu\MenuLinkInterface $link */
340 $link = $element->link;
342 $id = 'menu_plugin_id:' . $link->getPluginId();
343 $form[$id]['#item'] = $element;
344 $form[$id]['#attributes'] = $link->isEnabled() ? ['class' => ['menu-enabled']] : ['class' => ['menu-disabled']];
345 $form[$id]['title'] = Link::fromTextAndUrl($link->getTitle(), $link->getUrlObject())->toRenderable();
346 if (!$link->isEnabled()) {
347 $form[$id]['title']['#suffix'] = ' (' . $this->t('disabled') . ')';
349 // @todo Remove this in https://www.drupal.org/node/2568785.
350 elseif ($id === 'menu_plugin_id:user.logout') {
351 $form[$id]['title']['#suffix'] = ' (' . $this->t('<q>Log in</q> for anonymous users') . ')';
353 // @todo Remove this in https://www.drupal.org/node/2568785.
354 elseif (($url = $link->getUrlObject()) && $url->isRouted() && $url->getRouteName() == 'user.page') {
355 $form[$id]['title']['#suffix'] = ' (' . $this->t('logged in users only') . ')';
358 $form[$id]['enabled'] = [
359 '#type' => 'checkbox',
360 '#title' => $this->t('Enable @title menu link', ['@title' => $link->getTitle()]),
361 '#title_display' => 'invisible',
362 '#default_value' => $link->isEnabled(),
364 $form[$id]['weight'] = [
367 '#default_value' => $link->getWeight(),
368 '#title' => $this->t('Weight for @title', ['@title' => $link->getTitle()]),
369 '#title_display' => 'invisible',
373 '#value' => $link->getPluginId(),
375 $form[$id]['parent'] = [
377 '#default_value' => $link->getParent(),
379 // Build a list of operations.
381 $operations['edit'] = [
382 'title' => $this->t('Edit'),
384 // Allow for a custom edit link per plugin.
385 $edit_route = $link->getEditRoute();
387 $operations['edit']['url'] = $edit_route;
388 // Bring the user back to the menu overview.
389 $operations['edit']['query'] = $this->getDestinationArray();
392 // Fall back to the standard edit link.
393 $operations['edit'] += [
394 'url' => Url::fromRoute('menu_ui.link_edit', ['menu_link_plugin' => $link->getPluginId()]),
397 // Links can either be reset or deleted, not both.
398 if ($link->isResettable()) {
399 $operations['reset'] = [
400 'title' => $this->t('Reset'),
401 'url' => Url::fromRoute('menu_ui.link_reset', ['menu_link_plugin' => $link->getPluginId()]),
404 elseif ($delete_link = $link->getDeleteRoute()) {
405 $operations['delete']['url'] = $delete_link;
406 $operations['delete']['query'] = $this->getDestinationArray();
407 $operations['delete']['title'] = $this->t('Delete');
409 if ($link->isTranslatable()) {
410 $operations['translate'] = [
411 'title' => $this->t('Translate'),
412 'url' => $link->getTranslateRoute(),
415 $form[$id]['operations'] = [
416 '#type' => 'operations',
417 '#links' => $operations,
421 if ($element->subtree) {
422 $this->buildOverviewTreeForm($element->subtree, $delta);
426 $tree_access_cacheability
427 ->merge(CacheableMetadata::createFromRenderArray($form))
434 * Submit handler for the menu overview form.
436 * This function takes great care in saving parent items first, then items
437 * underneath them. Saving items in the incorrect order can break the tree.
439 protected function submitOverviewForm(array $complete_form, FormStateInterface $form_state) {
440 // Form API supports constructing and validating self-contained sections
441 // within forms, but does not allow to handle the form section's submission
442 // equally separated yet. Therefore, we use a $form_state key to point to
443 // the parents of the form section.
444 $parents = $form_state->get('menu_overview_form_parents');
445 $input = NestedArray::getValue($form_state->getUserInput(), $parents);
446 $form = &NestedArray::getValue($complete_form, $parents);
448 // When dealing with saving menu items, the order in which these items are
449 // saved is critical. If a changed child item is saved before its parent,
450 // the child item could be saved with an invalid path past its immediate
451 // parent. To prevent this, save items in the form in the same order they
452 // are sent, ensuring parents are saved first, then their children.
453 // See https://www.drupal.org/node/181126#comment-632270.
454 $order = is_array($input) ? array_flip(array_keys($input)) : [];
455 // Update our original form with the new order.
456 $form = array_intersect_key(array_merge($order, $form), $form);
458 $fields = ['weight', 'parent', 'enabled'];
459 $form_links = $form['links'];
460 foreach (Element::children($form_links) as $id) {
461 if (isset($form_links[$id]['#item'])) {
462 $element = $form_links[$id];
463 $updated_values = [];
464 // Update any fields that have changed in this menu item.
465 foreach ($fields as $field) {
466 if ($element[$field]['#value'] != $element[$field]['#default_value']) {
467 $updated_values[$field] = $element[$field]['#value'];
470 if ($updated_values) {
471 // Use the ID from the actual plugin instance since the hidden value
472 // in the form could be tampered with.
473 $this->menuLinkManager->updateDefinition($element['#item']->link->getPLuginId(), $updated_values);