diff core/modules/menu_ui/src/MenuForm.php @ 0:c75dbcec494b

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