class FormHooks

Same name and namespace in other branches
  1. main core/themes/admin/src/Hook/FormHooks.php \Drupal\admin\Hook\FormHooks

Provides form related hook implementations.

Hierarchy

Expanded class hierarchy of FormHooks

File

core/themes/admin/src/Hook/FormHooks.php, line 32

Namespace

Drupal\admin\Hook
View source
class FormHooks {
  use AjaxHelperTrait;
  use StringTranslationTrait;
  
  /**
   * Constructs the form related hooks.
   */
  public function __construct(protected ClassResolverInterface $classResolver, protected readonly ModuleHandlerInterface $moduleHandler, protected readonly ConfigFactoryInterface $configFactory, protected readonly StateInterface $state, protected readonly MessengerInterface $messenger, protected RouteMatchInterface $routeMatch, protected AccountInterface $currentUser) {
  }
  
  /**
   * Implements hook_form_alter().
   */
  public function formAlter(array &$form, FormStateInterface $form_state, string $form_id) : void {
    $build_info = $form_state->getBuildInfo();
    $form_object = $form_state->getFormObject();
    // Make entity forms delete link use the action-link component.
    if (isset($form['actions']['delete']['#type']) && $form['actions']['delete']['#type'] === 'link' && !empty($build_info['callback_object']) && $build_info['callback_object'] instanceof EntityForm) {
      $form['actions']['delete'] = Helper::convertLinkToActionLink($form['actions']['delete'], 'trash', 'default', 'danger');
    }
    if (isset($form['actions']['delete_translation']['#type']) && $form['actions']['delete_translation']['#type'] === 'link' && !empty($build_info['callback_object']) && $build_info['callback_object'] instanceof EntityForm) {
      $form['actions']['delete_translation'] = Helper::convertLinkToActionLink($form['actions']['delete_translation'], 'trash', 'default', 'danger');
    }
    if (($form_object instanceof ViewsForm || $form_object instanceof ViewsFormInterface) && isset($form['override']['#prefix'])) {
      // Replace form--inline class so positioning of override form elements
      // don't have to depend on floats.
      $form['override']['#prefix'] = str_replace('form--inline', 'form--flex', $form['override']['#prefix']);
    }
    if ($form_object instanceof ViewsForm && str_starts_with($form_object->getBaseFormId(), 'views_form_media_library')) {
      if (isset($form['header'])) {
        $form['header']['#attributes']['class'][] = 'media-library-views-form__header';
        $form['header']['media_bulk_form']['#attributes']['class'][] = 'media-library-views-form__bulk_form';
      }
      $form['actions']['submit']['#attributes']['class'] = [
        'media-library-select',
      ];
      $form['#attributes']['class'][] = 'media-library-views-form';
    }
    if ($form_object instanceof ViewsForm && !empty($form['header'])) {
      $view = $form_state->getBuildInfo()['args'][0];
      $view_title = $view->getTitle();
      // Determine if the Views form includes a bulk operations form. If it
      // does, move it to the bottom and remove the second bulk operations
      // submit.
      foreach (Element::children($form['header']) as $key) {
        if (str_contains($key, '_bulk_form')) {
          // Move the bulk actions form from the header to its own container.
          $form['bulk_actions_container'] = $form['header'][$key];
          // Remove the supplementary bulk operations submit button as it
          // appears in the same location the form was moved to.
          unset($form['header'][$key], $form['actions']);
          $form['bulk_actions_container']['#attributes']['data-drupal-views-bulk-actions'] = '';
          $form['bulk_actions_container']['#attributes']['class'][] = 'views-bulk-actions';
          $form['bulk_actions_container']['actions']['submit']['#button_type'] = 'primary';
          $form['bulk_actions_container']['actions']['submit']['#attributes']['class'][] = 'button--small';
          $label = $this->t('Perform actions on the selected items in the %view_title view', [
            '%view_title' => $view_title,
          ]);
          $label_id = $key . '_group_label';
          // Group the bulk actions select and submit elements, and add a label
          // that makes the purpose of these elements more clear to screen
          // readers.
          $form['bulk_actions_container']['#attributes']['role'] = 'group';
          $form['bulk_actions_container']['#attributes']['aria-labelledby'] = $label_id;
          $form['bulk_actions_container']['group_label'] = [
            '#type' => 'container',
            '#markup' => $label,
            '#attributes' => [
              'id' => $label_id,
              'class' => [
                'visually-hidden',
              ],
            ],
            '#weight' => -1,
          ];
          // Add a status label for counting the number of items selected.
          $form['bulk_actions_container']['status'] = [
            '#type' => 'container',
            '#markup' => $this->t('No items selected'),
            '#weight' => -1,
            '#attributes' => [
              'class' => [
                'js-views-bulk-actions-status',
                'views-bulk-actions__item',
                'views-bulk-actions__item--status',
                'js-show',
              ],
              'data-drupal-views-bulk-actions-status' => '',
            ],
          ];
          // Loop through bulk actions items and add the needed CSS classes.
          $bulk_action_item_keys = Element::children($form['bulk_actions_container'], TRUE);
          $bulk_last_key = NULL;
          $bulk_child_before_actions_key = NULL;
          foreach ($bulk_action_item_keys as $bulk_action_item_key) {
            if (!empty($form['bulk_actions_container'][$bulk_action_item_key]['#type'])) {
              if ($form['bulk_actions_container'][$bulk_action_item_key]['#type'] === 'actions') {
                // We need the key of the element that precedes the actions
                // element.
                $bulk_child_before_actions_key = $bulk_last_key;
                $form['bulk_actions_container'][$bulk_action_item_key]['#attributes']['class'][] = 'views-bulk-actions__item';
              }
              if (!in_array($form['bulk_actions_container'][$bulk_action_item_key]['#type'], [
                'hidden',
                'actions',
              ])) {
                $form['bulk_actions_container'][$bulk_action_item_key]['#wrapper_attributes']['class'][] = 'views-bulk-actions__item';
                $bulk_last_key = $bulk_action_item_key;
              }
            }
          }
          if ($bulk_child_before_actions_key) {
            $form['bulk_actions_container'][$bulk_child_before_actions_key]['#wrapper_attributes']['class'][] = 'views-bulk-actions__item--preceding-actions';
          }
        }
      }
    }
    $this->stickyActionButtonsAndSidebar($form, $form_state, $form_id);
    // Bulk forms: update action & actions to small variants.
    if (isset($form['header']) && str_contains($form_id, 'views_form')) {
      $bulk_form = current(preg_grep('/_bulk_form/', array_keys($form['header'])));
      if (isset($form['header'][$bulk_form])) {
        $form['header'][$bulk_form]['action']['#attributes']['class'][] = 'form-element--type-select--small';
        $form['header'][$bulk_form]['actions']['submit']['#attributes']['class'][] = 'button--small';
        // Remove double entry of submit button.
        unset($form['actions']['submit']);
      }
    }
    // Delete forms: alter buttons.
    if (str_contains($form_id, 'delete_form')) {
      $form['actions']['submit']['#attributes']['class'][] = 'button--danger';
      $form['actions']['cancel']['#attributes']['class'][] = 'button--secondary';
    }
  }
  
  /**
   * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\media\MediaForm.
   */
  public function formMediaFormAlter(array &$form, FormStateInterface $form_state) : void {
    $form['#attached']['library'][] = 'admin/media-form';
  }
  
  /**
   * Implements hook_form_BASE_FORM_ID_alter().
   */
  public function formMediaLibraryAddFormAlter(array &$form) : void {
    $form['#attributes']['class'][] = 'media-library-add-form';
    $form['#attached']['library'][] = 'admin/media_library.theme';
    // If there are unsaved media items, apply styling classes to various parts
    // of the form.
    if (isset($form['media'])) {
      $form['#attributes']['class'][] = 'media-library-add-form--with-input';
      // Put a wrapper around the informational message above the unsaved media
      // items.
      $form['description']['#template'] = '<p class="media-library-add-form__description">{{ text }}</p>';
    }
    else {
      $form['#attributes']['class'][] = 'media-library-add-form--without-input';
    }
  }
  
  /**
   * Implements hook_form_FORM_ID_alter().
   */
  public function formMediaLibraryAddFormOembedAlter(array &$form) : void {
    $form['#attributes']['class'][] = 'media-library-add-form--oembed';
    // If no media items have been added yet, add a couple of styling classes
    // to the initial URL form.
    if (isset($form['container'])) {
      $form['container']['#attributes']['class'][] = 'media-library-add-form__input-wrapper';
      $form['container']['url']['#attributes']['class'][] = 'media-library-add-form-oembed-url';
      $form['container']['submit']['#attributes']['class'][] = 'media-library-add-form-oembed-submit';
    }
  }
  
  /**
   * Implements hook_form_FORM_ID_alter().
   */
  public function formMediaLibraryAddFormUploadAlter(array &$form) : void {
    $form['#attributes']['class'][] = 'media-library-add-form--upload';
    if (isset($form['container']['upload'])) {
      // Set this flag so we can prevent the details element from being added
      // in \Drupal\admin\Hook\ThemeHooks::managedFile.
      $form['container']['upload']['#do_not_wrap_in_details'] = TRUE;
    }
    if (isset($form['container'])) {
      $form['container']['#attributes']['class'][] = 'media-library-add-form__input-wrapper';
    }
  }
  
  /**
   * Implements hook_form_BASE_FORM_ID_alter() for MenuLinkContentForm.
   *
   * Alters the menu_link_content_form by organizing form elements into
   * different 'details' sections.
   */
  public function formMenuLinkContentFormAlter(array &$form) : void {
    $form['#theme'] = [
      'menu_link_form',
    ];
    $form['#attached']['library'][] = 'admin/form-two-columns';
    $form['advanced'] = [
      '#type' => 'container',
      '#weight' => 10,
      '#accordion' => TRUE,
    ];
    $form['menu_parent']['#wrapper_attributes'] = [
      'class' => [
        'accordion__item',
        'entity-meta__header',
      ],
    ];
    $form['menu_parent']['#prefix'] = '<div class="accordion">';
    $form['menu_parent']['#suffix'] = '</div>';
    $form['menu_parent']['#group'] = 'advanced';
    $form['menu_link_display_settings'] = [
      '#type' => 'details',
      '#group' => 'advanced',
      '#title' => $this->t('Display settings'),
      '#attributes' => [
        'class' => [
          'entity-meta__options',
        ],
      ],
      '#tree' => FALSE,
      '#accordion' => TRUE,
    ];
    if (!empty($form['weight'])) {
      $form['menu_link_display_settings']['weight'] = $form['weight'];
      unset($form['weight'], $form['menu_link_display_settings']['weight']['#weight']);
    }
    if (!empty($form['expanded'])) {
      $form['menu_link_display_settings']['expanded'] = $form['expanded'];
      unset($form['expanded']);
    }
    if (isset($form['description'])) {
      $form['menu_link_description'] = [
        '#type' => 'details',
        '#group' => 'advanced',
        '#title' => $this->t('Description'),
        '#attributes' => [
          'class' => [
            'entity-meta__description',
          ],
        ],
        '#tree' => FALSE,
        '#accordion' => TRUE,
        'description' => $form['description'],
      ];
      unset($form['description']);
    }
  }
  
  /**
   * Implements hook_form_FORM_ID_alter() for MenuLinkEditForm.
   *
   * Alters the menu_link_edit form by organizing form elements into different
   * 'details' sections.
   */
  public function formMenuLinkEditAlter(array &$form) : void {
    $this->formMenuLinkContentFormAlter($form);
  }
  
  /**
   * Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\Form\NodeForm.
   *
   * Changes vertical tabs to container.
   */
  public function formNodeFormAlter(array &$form) : void {
    $form['#theme'] = [
      'node_edit_form',
    ];
    $form['#attached']['library'][] = 'admin/form-two-columns';
    $this->ensureAdvancedSettings($form);
  }
  
  /**
   * Implements hook_form_FORM_ID_alter().
   */
  public function formNodePreviewFormSelectAlter(array &$form) : void {
    if (isset($form['backlink'])) {
      $form['backlink']['#options']['attributes']['class'][] = 'action-link';
      $form['backlink']['#options']['attributes']['class'][] = 'action-link--icon-chevron-left';
    }
  }
  
  /**
   * Implements hook_form_FORM_ID_alter() for the system_modules form.
   */
  public function formSystemModulesAlter(array &$form) : void {
    if (isset($form['filters'])) {
      $form['filters']['#attributes']['class'][] = 'modules-table-filter';
      if (isset($form['filters']['text'])) {
        unset($form['filters']['text']['#title_display']);
        $form['filters']['text']['#title'] = $this->t('Filter');
      }
    }
    // Convert module links to action links.
    foreach (Element::children($form['modules']) as $key) {
      $link_key_to_action_link_type = [
        'help' => 'questionmark',
        'permissions' => 'key',
        'configure' => 'cog',
      ];
      if (isset($form['modules'][$key]['#type']) && $form['modules'][$key]['#type'] === 'details') {
        $form['modules'][$key]['#module_package_listing'] = TRUE;
        foreach (Element::children($form['modules'][$key]) as $module_key) {
          if (isset($form['modules'][$key][$module_key]['links'])) {
            foreach ($form['modules'][$key][$module_key]['links'] as $link_key => &$link) {
              if (array_key_exists($link_key, $link_key_to_action_link_type)) {
                $action_link_type = $link_key_to_action_link_type[$link_key];
                $link['#options']['attributes']['class'][] = 'action-link';
                $link['#options']['attributes']['class'][] = 'action-link--small';
                $link['#options']['attributes']['class'][] = "action-link--icon-{$action_link_type}";
              }
            }
          }
        }
      }
    }
  }
  
  /**
   * Implement hook form_system_theme_settings_alter().
   */
  public function formSystemThemeSettingsAlter(array &$form) : void {
    if (!isset($form['config_key']['#value']) || $form['config_key']['#value'] !== 'admin.settings') {
      return;
    }
    $settings = Settings::getInstance();
    // Move default theme settings to bottom.
    $form['logo']['#open'] = FALSE;
    $form['logo']['#weight'] = 97;
    $form['favicon']['#open'] = FALSE;
    $form['favicon']['#weight'] = 98;
    $form['theme_settings']['#open'] = FALSE;
    $form['theme_settings']['#weight'] = 99;
    // General settings.
    $form['custom_settings'] = [
      '#type' => 'details',
      '#open' => TRUE,
      '#title' => $this->t('Settings'),
    ] + $settings->getSettingsForm();
    // Allow user settings.
    $form['custom_settings']['show_user_theme_settings'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Users can override admin settings'),
      '#description' => $this->t('Expose the admin theme settings to users.'),
      '#default_value' => $settings->getDefault('show_user_theme_settings'),
    ];
    // Add handler.
    $form['#validate'][] = [
      __CLASS__,
      'formSystemThemeSettingsAlterValidate',
    ];
    $form['#submit'][] = [
      __CLASS__,
      'formSystemThemeSettingsAlterSubmit',
    ];
    // Attach custom library.
    $form['#attached']['library'][] = 'admin/settings';
  }
  
  /**
   * Validate theme settings.
   */
  public static function formSystemThemeSettingsAlterValidate(array &$form, FormStateInterface $form_state) : void {
  }
  
  /**
   * Submit theme settings.
   */
  public static function formSystemThemeSettingsAlterSubmit(array &$form, FormStateInterface $form_state) : void {
  }
  
  /**
   * Implements hook_form_FORM_ID_alter() for the user_admin_permissions form.
   */
  public function formUserAdminPermissionsAlter(array &$form) : void {
    if (isset($form['filters'])) {
      $form['filters']['#attributes']['class'][] = 'permissions-table-filter';
      if (isset($form['filters']['text'])) {
        unset($form['filters']['text']['#title_display']);
        $form['filters']['text']['#title'] = $this->t('Filter');
      }
    }
  }
  
  /**
   * Implements form_user_form_alter().
   */
  public function formUserFormAlter(array &$form, FormStateInterface $form_state) : void {
    // If new user account, don't show settings yet.
    $formObject = $form_state->getFormObject();
    if ($formObject instanceof EntityFormInterface && $formObject->getEntity()
      ->isNew()) {
      return;
    }
    if (Settings::getInstance()->allowUserOverrides()) {
      // Inject the settings for the dark mode feature.
      $form['admin_theme_settings'] = [
        '#type' => 'details',
        '#title' => $this->t('Admin theme settings'),
        '#open' => TRUE,
        '#weight' => 90,
      ];
      /** @var \Drupal\Core\Session\AccountInterface $account */
      $account = $form_state->getBuildInfo()['callback_object']
        ->getEntity();
      $form['admin_theme_settings']['enable_user_settings'] = [
        '#type' => 'checkbox',
        '#title' => $this->t('Enable overrides'),
        '#description' => $this->t("Enables default admin theme overrides."),
        '#default_value' => Settings::getInstance()->userOverrideEnabled($account),
        '#weight' => 0,
      ];
      $form['admin_theme_settings']['user_settings'] = [
        '#type' => 'container',
        '#states' => [
          // Show if met.
'visible' => [
            ':input[name="enable_user_settings"]' => [
              'checked' => TRUE,
            ],
          ],
        ],
      ] + Settings::getInstance()->getSettingsForm($account);
      // Attach custom library.
      $form['#attached']['library'][] = 'admin/settings';
      array_unshift($form['actions']['submit']['#submit'], [
        __CLASS__,
        'userFormSubmit',
      ]);
    }
  }
  
  /**
   * Submit handler for the user form.
   */
  public static function userFormSubmit(array &$form, FormStateInterface $form_state) : void {
    /** @var \Drupal\Core\Session\AccountInterface $account */
    $account = $form_state->getBuildInfo()['callback_object']
      ->getEntity();
    $settings = \Drupal::classResolver(Settings::class);
    $enabledUserOverrides = $form_state->getValue('enable_user_settings');
    if ($enabledUserOverrides) {
      $user_settings = [
        'preset_accent_color' => $form_state->getValue('preset_accent_color'),
        'preset_focus_color' => $form_state->getValue('preset_focus_color'),
        'enable_dark_mode' => $form_state->getValue('enable_dark_mode'),
        'high_contrast_mode' => (bool) $form_state->getValue('high_contrast_mode'),
        'accent_color' => $form_state->getValue('accent_color'),
        'focus_color' => $form_state->getValue('focus_color'),
        'layout_density' => $form_state->getValue('layout_density'),
        'show_description_toggle' => $form_state->getValue('show_description_toggle'),
      ];
      $settings->setAll($user_settings, $account);
    }
    else {
      $settings->clear($account);
    }
  }
  
  /**
   * Implements hook_form_FORM_ID_alter() for views_exposed_form.
   */
  public function formViewsExposedFormAlter(array &$form, FormStateInterface $form_state) : void {
    $view = $form_state->getStorage()['view'];
    $view_title = $view->getTitle();
    // Add a label so screen readers can identify the purpose of the exposed
    // form without having to scan content that appears further down the page.
    $form['#attributes']['aria-label'] = $this->t('Filter the contents of the %view_title view', [
      '%view_title' => $view_title,
    ]);
  }
  
  /**
   * Implements hook_form_FORM_ID_alter() for Views UI add handler form.
   */
  public function formViewsUiAddHandlerFormAlter(array &$form) : void {
    // Remove container-inline class to allow more control over styling.
    if (isset($form['selected']['#attributes']['class'])) {
      $form['selected']['#attributes']['class'] = array_diff($form['selected']['#attributes']['class'], [
        'container-inline',
      ]);
    }
    // Move all form elements in controls to its parent, this places them all in
    // the same div, which makes it possible to position them with flex styling
    // instead of floats.
    if (isset($form['override']['controls'])) {
      foreach (Element::children($form['override']['controls']) as $key) {
        $form['override']["controls_{$key}"] = $form['override']['controls'][$key];
        // The wrapper array for controls is removed after this loop completes.
        // The wrapper ensured that its child elements were hidden in browsers
        // without JavaScript. To replicate this functionality, the `.js-show`
        // class is added to each item previously contained in the wrapper.
        if (isset($form['override']['controls']['#id']) && $form['override']['controls']['#id'] === 'views-filterable-options-controls') {
          $form['override']["controls_{$key}"]['#wrapper_attributes']['class'][] = 'js-show';
        }
      }
      unset($form['override']['controls']);
    }
  }
  
  /**
   * Implements hook_form_FORM_ID_alter() for the Views UI config form.
   */
  public function formViewsUiConfigItemFormAlter(array &$form, FormStateInterface $form_state) : void {
    $type = $form_state->get('type');
    if ($type === 'filter') {
      // Remove clearfix classes from several elements. They add unwanted
      // whitespace and are no longer needed because uses of `float:` in this
      // form have been removed.
      // @todo Many of the changes to classes within this conditional may not be
      //   needed or require refactoring in https://drupal.org/node/3164890
      unset($form['options']['clear_markup_start'], $form['options']['clear_markup_end']);
      if (isset($form['options']['expose_button']['#prefix'])) {
        $form['options']['expose_button']['#prefix'] = str_replace('clearfix', '', $form['options']['expose_button']['#prefix']);
      }
      if (isset($form['options']['group_button']['#prefix'])) {
        $form['options']['group_button']['#prefix'] = str_replace('clearfix', '', $form['options']['group_button']['#prefix']);
      }
      // Remove `views-(direction)-(amount)` classes, replace with
      // `views-group-box--operator`, and add a `views-config-group-region`
      // wrapper.
      if (isset($form['options']['operator']['#prefix'])) {
        foreach ([
          'views-left-30',
          'views-left-40',
        ] as $left_class) {
          if (str_contains($form['options']['operator']['#prefix'], $left_class)) {
            $form['options']['operator']['#prefix'] = '<div class="views-config-group-region">' . str_replace($left_class, 'views-group-box--operator', $form['options']['operator']['#prefix']);
            $form['options']['value']['#suffix'] = ($form['options']['value']['#suffix'] ?? '') . '</div>';
          }
        }
      }
      // Some instances of this form input have an added wrapper that needs to
      // be removed in order to style these forms consistently.
      // @see \Drupal\views\Plugin\views\filter\InOperator::valueForm
      $wrapper_div_to_remove = '<div id="edit-options-value-wrapper">';
      if (isset($form['options']['value']['#prefix']) && str_contains($form['options']['value']['#prefix'], $wrapper_div_to_remove)) {
        $form['options']['value']['#prefix'] = str_replace($wrapper_div_to_remove, '', $form['options']['value']['#prefix']);
        $form['options']['value']['#suffix'] = preg_replace('/<\\/div>/', '', $form['options']['value']['#suffix'], 1);
      }
      if (isset($form['options']['value']['#prefix'])) {
        foreach ([
          'views-right-70',
          'views-right-60',
        ] as $right_class) {
          if (str_contains($form['options']['value']['#prefix'], $right_class)) {
            $form['options']['value']['#prefix'] = str_replace($right_class, 'views-group-box--value', $form['options']['value']['#prefix']);
          }
        }
      }
      // If the form includes a `value` field, the `.views-group-box--value` and
      // `.views-group-box` classes must be present in a wrapper div. Add them
      // here if it they are not yet present.
      if (!isset($form['options']['value']['#prefix']) || !str_contains($form['options']['value']['#prefix'], 'views-group-box--value')) {
        $prefix = $form['options']['value']['#prefix'] ?? '';
        $suffix = $form['options']['value']['#suffix'] ?? '';
        $form['options']['value']['#prefix'] = '<div class="views-group-box views-group-box--value">' . $prefix;
        $form['options']['value']['#suffix'] = $suffix . '</div>';
      }
      // If operator or value have no children, remove them from the render
      // array so their prefixes and suffixes aren't added without any content.
      foreach ([
        'operator',
        'value',
      ] as $form_item) {
        if (isset($form['options'][$form_item]['#prefix']) && count($form['options'][$form_item]) === 2 && $form['options'][$form_item]['#suffix']) {
          unset($form['options'][$form_item]);
        }
      }
    }
  }
  
  /**
   * Add some major form overrides.
   *
   * @param array $form
   *   An associative array containing the structure of the form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   * @param string $form_id
   *   The form id.
   *
   * @see hook_form_alter()
   */
  public function stickyActionButtonsAndSidebar(array &$form, FormStateInterface $form_state, string $form_id) : void {
    if ($this->isModalOrOffcanvas()) {
      $form['is_ajax_request'] = [
        '#weight' => -1,
      ];
      return;
    }
    if (str_ends_with($form_id, '_exposed_form') || str_starts_with($form_id, 'views_ui_')) {
      return;
    }
    // Save form types and behaviors.
    $is_content_form = Helper::isContentForm($form_state, $form_id);
    // If there are action buttons, and they should either be sticky or there
    // is a content form, where the sidebar toggle is required, prepare the
    // sticky action container for the top bar.
    if (isset($form['actions']) && (self::useStickyActionButtons($is_content_form) || $is_content_form)) {
      // Sticky action container.
      $form['gin_sticky_actions'] = [
        '#type' => 'container',
        '#weight' => -1,
        '#multilingual' => TRUE,
        '#attributes' => [
          'class' => [
            'gin-sticky-form-actions',
          ],
        ],
      ];
      $form['#after_build'][] = [
        __CLASS__,
        'formAfterBuild',
      ];
    }
    // Sticky action buttons.
    if (self::useStickyActionButtons($is_content_form) && isset($form['actions'])) {
      // Add sticky class.
      $form['actions']['#attributes']['class'][] = 'gin-sticky-form-actions';
      // Add a class to identify modified forms.
      if (!isset($form['#attributes']['class'])) {
        $form['#attributes']['class'] = [];
      }
      elseif (is_string($form['#attributes']['class'])) {
        $form['#attributes']['class'] = [
          $form['#attributes']['class'],
        ];
      }
      $form['#attributes']['class'][] = 'gin--has-sticky-form-actions';
      // Assign status to gin_actions.
      $form['gin_sticky_actions']['status'] = [
        '#type' => 'container',
        '#weight' => -1,
        '#multilingual' => TRUE,
      ];
      // Only alter the status field on content forms.
      if ($is_content_form) {
        // Set form id to status field.
        if (isset($form['status']['widget']['value'])) {
          $form['status']['widget']['value']['#attributes']['form'] = $form['#id'];
          $widget_type = $form['status']['widget']['value']['#type'] ?? FALSE;
        }
        else {
          $widget_type = $form['status']['widget']['#type'] ?? FALSE;
        }
        // Only move status to status group if it is a checkbox.
        if ($widget_type === 'checkbox') {
          $form['status']['#group'] = 'status';
        }
      }
      // Helper item to move focus to sticky header.
      $form['gin_move_focus_to_sticky_bar'] = [
        '#markup' => '<a href="#" class="visually-hidden" role="button" gin-move-focus-to-sticky-bar>Moves focus to sticky header actions</a>',
        '#weight' => 999,
      ];
      // Attach library.
      $form['#attached']['library'][] = 'admin/more_actions';
    }
    // Remaining changes only apply to content forms.
    if (!$is_content_form) {
      return;
    }
    // Provide a default meta form element if not already provided.
    // @see NodeForm::form()
    $form['advanced']['#attributes']['class'][] = 'entity-meta';
    if (!isset($form['meta'])) {
      $form['meta'] = [
        '#group' => 'advanced',
        '#weight' => -10,
        '#title' => $this->t('Status'),
        '#attributes' => [
          'class' => [
            'entity-meta__header',
          ],
        ],
        '#tree' => TRUE,
      ];
    }
    $this->ensureAdvancedSettings($form);
    // Add sidebar toggle.
    $hide_panel = $this->t('Hide sidebar panel');
    $form['gin_sticky_actions']['gin_sidebar_toggle'] = [
      '#markup' => '<a href="#toggle-sidebar" class="meta-sidebar__trigger trigger" role="button" title="' . $hide_panel . '" aria-controls="gin_sidebar"><span class="visually-hidden">' . $hide_panel . '</span></a>',
      '#weight' => 1000,
    ];
    $form['#attached']['library'][] = 'admin/sidebar';
    // Create gin_sidebar group.
    $form['gin_sidebar'] = [
      '#group' => 'meta',
      '#type' => 'container',
      '#weight' => 99,
      '#multilingual' => TRUE,
      '#attributes' => [
        'class' => [
          'gin-sidebar',
        ],
      ],
    ];
    // Copy footer over.
    $form['gin_sidebar']['footer'] = $form['footer'] ?? [];
    // Sidebar close button.
    $close_sidebar_translation = $this->t('Close sidebar panel');
    $form['gin_sidebar']['gin_sidebar_close'] = [
      '#markup' => '<a href="#close-sidebar" class="meta-sidebar__close trigger" role="button" title="' . $close_sidebar_translation . '"><span class="visually-hidden">' . $close_sidebar_translation . '</span></a>',
    ];
    $form['gin_sidebar_overlay'] = [
      '#markup' => '<div class="meta-sidebar__overlay trigger"></div>',
    ];
    // Specify necessary node form theme and library.
    // @see gin_form_node_form_alter
    $form['#theme'] = [
      'node_edit_form',
    ];
    // Attach libraries.
    $form['#attached']['library'][] = 'admin/node-form';
    $form['#attached']['library'][] = 'admin/edit_form';
    // Add a class that allows the logic in edit_form.js to identify the form.
    $form['#attributes']['class'][] = 'gin-node-edit-form';
    // If not logged in hide changed and author node info on add forms.
    $not_logged_in = $this->currentUser
      ->isAnonymous();
    $route = $this->routeMatch
      ->getRouteName();
    if ($not_logged_in && $route === 'node.add') {
      unset($form['meta']['changed'], $form['meta']['author']);
    }
  }
  
  /**
   * Determines the feature flag to use for sticky action buttons.
   *
   * @param bool $is_content_form
   *   TRUE if this should return the flag for content forms, FALSE if it should
   *   return the flag for any form.
   *
   * @return bool
   *   TRUE if core_admin_theme_use_sticky_action_buttons is enabled, FALSE
   *   otherwise.
   */
  private static function useStickyActionButtons(bool $is_content_form = TRUE) : bool {
    $flag = CoreSettings::get('core_admin_theme_use_sticky_action_buttons', 'never');
    return $is_content_form ? $flag === 'content_forms' || $flag === 'always' : $flag === 'always';
  }
  
  /**
   * Check the context we're in.
   *
   * Checks if the form is in either a modal or an off-canvas dialog.
   */
  private function isModalOrOffcanvas() : bool {
    $wrapper_format = $this->getRequestWrapperFormat() ?? '';
    return str_contains($wrapper_format, 'drupal_modal') || str_contains($wrapper_format, 'drupal_dialog');
  }
  
  /**
   * Helper function to remember the form actions after form has been built.
   */
  public static function formAfterBuild(array $form) : array {
    if (self::useStickyActionButtons()) {
      // Allowlist for visible actions.
      $includes = [
        'save',
        'submit',
        'preview',
      ];
      // Build actions.
      foreach (Element::children($form['actions']) as $key) {
        $button = $form['actions'][$key] ?? [];
        if (!($button['#access'] ?? TRUE)) {
          continue;
        }
        $navigation_enabled = \Drupal::service('module_handler')->moduleExists('navigation');
        if ($navigation_enabled) {
          $form['gin_sticky_actions']['actions'][$key] = $button;
        }
        // The media_type_add_form form is a special case.
        // @see https://www.drupal.org/project/gin/issues/3534385
        // @see \Drupal\media\MediaTypeForm::actions
        if (isset($button['#type']) && $button['#type'] === 'submit' || $form['#form_id'] === 'media_type_add_form') {
          // Update button.
          $button['#attributes']['id'] = 'gin-sticky-' . $button['#id'];
          $button['#attributes']['form'] = $form['#id'];
          $button['#attributes']['data-drupal-selector'] = 'gin-sticky-' . $button['#attributes']['data-drupal-selector'];
          $button['#attributes']['data-gin-sticky-form-selector'] = $button['#attributes']['data-drupal-selector'];
          // Add the button to the form actions array.
          if (!empty($button['#gin_action_item']) || $navigation_enabled || in_array($key, $includes, TRUE)) {
            $form['gin_sticky_actions']['actions'][$key] = $button;
          }
        }
      }
    }
    Helper::formActions($form['gin_sticky_actions'] ?? NULL);
    unset($form['gin_sticky_actions']);
    return $form;
  }
  
  /**
   * Ensure correct settings for advanced, meta and revision form elements.
   *
   * @param array $form
   *   The form.
   */
  private function ensureAdvancedSettings(array &$form) : void {
    $form['advanced']['#type'] = 'container';
    $form['advanced']['#accordion'] = TRUE;
    $form['meta']['#type'] = 'container';
    $form['meta']['#access'] = TRUE;
    $form['revision_information']['#type'] = 'container';
    $form['revision_information']['#group'] = 'meta';
    $form['revision_information']['#attributes']['class'][] = 'entity-meta__revision';
  }

}

Members

Title Sort descending Modifiers Object type Summary Overrides
AjaxHelperTrait::getRequestWrapperFormat protected function Gets the wrapper format of the current request.
AjaxHelperTrait::isAjax protected function Determines if the current request is via AJAX.
FormHooks::ensureAdvancedSettings private function Ensure correct settings for advanced, meta and revision form elements.
FormHooks::formAfterBuild public static function Helper function to remember the form actions after form has been built.
FormHooks::formAlter public function Implements hook_form_alter().
FormHooks::formMediaFormAlter public function Implements hook_form_BASE_FORM_ID_alter() for \Drupal\media\MediaForm.
FormHooks::formMediaLibraryAddFormAlter public function Implements hook_form_BASE_FORM_ID_alter().
FormHooks::formMediaLibraryAddFormOembedAlter public function Implements hook_form_FORM_ID_alter().
FormHooks::formMediaLibraryAddFormUploadAlter public function Implements hook_form_FORM_ID_alter().
FormHooks::formMenuLinkContentFormAlter public function Implements hook_form_BASE_FORM_ID_alter() for MenuLinkContentForm.
FormHooks::formMenuLinkEditAlter public function Implements hook_form_FORM_ID_alter() for MenuLinkEditForm.
FormHooks::formNodeFormAlter public function Implements hook_form_BASE_FORM_ID_alter() for \Drupal\node\Form\NodeForm.
FormHooks::formNodePreviewFormSelectAlter public function Implements hook_form_FORM_ID_alter().
FormHooks::formSystemModulesAlter public function Implements hook_form_FORM_ID_alter() for the system_modules form.
FormHooks::formSystemThemeSettingsAlter public function Implement hook form_system_theme_settings_alter().
FormHooks::formSystemThemeSettingsAlterSubmit public static function Submit theme settings.
FormHooks::formSystemThemeSettingsAlterValidate public static function Validate theme settings.
FormHooks::formUserAdminPermissionsAlter public function Implements hook_form_FORM_ID_alter() for the user_admin_permissions form.
FormHooks::formUserFormAlter public function Implements form_user_form_alter().
FormHooks::formViewsExposedFormAlter public function Implements hook_form_FORM_ID_alter() for views_exposed_form.
FormHooks::formViewsUiAddHandlerFormAlter public function Implements hook_form_FORM_ID_alter() for Views UI add handler form.
FormHooks::formViewsUiConfigItemFormAlter public function Implements hook_form_FORM_ID_alter() for the Views UI config form.
FormHooks::isModalOrOffcanvas private function Check the context we&#039;re in.
FormHooks::stickyActionButtonsAndSidebar public function Add some major form overrides.
FormHooks::userFormSubmit public static function Submit handler for the user form.
FormHooks::useStickyActionButtons private static function Determines the feature flag to use for sticky action buttons.
FormHooks::__construct public function Constructs the form related hooks.
StringTranslationTrait::$stringTranslation protected property The string translation service. 3
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language. 1

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.