You are here

public function MediaLibraryWidget::formElement in Drupal 10

Same name and namespace in other branches
  1. 8 core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::formElement()
  2. 9 core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::formElement()

Returns the form for a single field widget.

Field widget form elements should be based on the passed-in $element, which contains the base form element properties derived from the field configuration.

The BaseWidget methods will set the weight, field name and delta values for each form element. If there are multiple values for this field, the formElement() method will be called as many times as needed.

Other modules may alter the form element provided by this function using hook_field_widget_single_element_form_alter() or hook_field_widget_single_element_WIDGET_TYPE_form_alter().

The FAPI element callbacks (such as #process, #element_validate, #value_callback, etc.) used by the widget do not have access to the original $field_definition passed to the widget's constructor. Therefore, if any information is needed from that definition by those callbacks, the widget implementing this method, or a hook_field_widget[_WIDGET_TYPE]_form_alter() implementation, must extract the needed properties from the field definition and set them as ad-hoc $element['#custom'] properties, for later use by its element callbacks.

Parameters

\Drupal\Core\Field\FieldItemListInterface $items: Array of default values for this field.

int $delta: The order of this item in the array of sub-elements (0, 1, 2, etc.).

array $element: A form element array containing basic properties for the widget:

  • #field_parents: The 'parents' space for the field in the form. Most widgets can simply overlook this property. This identifies the location where the field values are placed within $form_state->getValues(), and is used to access processing information for the field through the getWidgetState() and setWidgetState() methods.
  • #title: The sanitized element label for the field, ready for output.
  • #description: The sanitized element description for the field, ready for output.
  • #required: A Boolean indicating whether the element value is required; for required multiple value fields, only the first widget's values are required.
  • #delta: The order of this item in the array of sub-elements; see $delta above.

array $form: The form structure where widgets are being attached to. This might be a full form structure, or a sub-element of a larger form.

\Drupal\Core\Form\FormStateInterface $form_state: The current state of the form.

Return value

array The form elements for a single widget for this field.

Overrides WidgetInterface::formElement

See also

hook_field_widget_single_element_form_alter()

hook_field_widget_single_element_WIDGET_TYPE_form_alter()

File

core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php, line 308

Class

MediaLibraryWidget
Plugin implementation of the 'media_library_widget' widget.

Namespace

Drupal\media_library\Plugin\Field\FieldWidget

Code

public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {

  /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items */
  $referenced_entities = $items
    ->referencedEntities();
  $view_builder = $this->entityTypeManager
    ->getViewBuilder('media');
  $field_name = $this->fieldDefinition
    ->getName();
  $parents = $form['#parents'];

  // Create an ID suffix from the parents to make sure each widget is unique.
  $id_suffix = $parents ? '-' . implode('-', $parents) : '';
  $field_widget_id = implode(':', array_filter([
    $field_name,
    $id_suffix,
  ]));
  $wrapper_id = $field_name . '-media-library-wrapper' . $id_suffix;
  $limit_validation_errors = [
    array_merge($parents, [
      $field_name,
    ]),
  ];
  $settings = $this
    ->getFieldSetting('handler_settings');
  $element += [
    '#type' => 'fieldset',
    '#cardinality' => $this->fieldDefinition
      ->getFieldStorageDefinition()
      ->getCardinality(),
    // If no target bundles are specified, all target bundles are allowed.
    '#target_bundles' => $settings['target_bundles'] ?? [],
    '#attributes' => [
      'id' => $wrapper_id,
      'class' => [
        'js-media-library-widget',
      ],
    ],
    '#pre_render' => [
      [
        $this,
        'preRenderWidget',
      ],
    ],
    '#attached' => [
      'library' => [
        'media_library/widget',
      ],
    ],
    '#theme_wrappers' => [
      'fieldset__media_library_widget',
    ],
  ];
  if ($this->fieldDefinition
    ->isRequired()) {
    $element['#element_validate'][] = [
      static::class,
      'validateRequired',
    ];
  }

  // When the list of allowed types in the field configuration is null,
  // ::getAllowedMediaTypeIdsSorted() returns all existing media types. When
  // the list of allowed types is an empty array, we show a message to users
  // and ask them to configure the field if they have access.
  $allowed_media_type_ids = $this
    ->getAllowedMediaTypeIdsSorted();
  if (!$allowed_media_type_ids) {
    $element['no_types_message'] = [
      '#markup' => $this
        ->getNoMediaTypesAvailableMessage(),
    ];
    return $element;
  }
  $multiple_items = FALSE;
  if (empty($referenced_entities)) {
    $element['#field_prefix']['empty_selection'] = [
      '#markup' => $this
        ->t('No media items are selected.'),
    ];
  }
  else {

    // @todo Use a <button> link here, and delete
    // seven_preprocess_fieldset__media_library_widget(), when
    // https://www.drupal.org/project/drupal/issues/2999549 lands.
    $multiple_items = count($referenced_entities) > 1;
    $element['#field_prefix']['weight_toggle'] = [
      '#type' => 'html_tag',
      '#tag' => 'button',
      '#value' => $this
        ->t('Show media item weights'),
      '#access' => $multiple_items,
      '#attributes' => [
        'class' => [
          'link',
          'js-media-library-widget-toggle-weight',
        ],
      ],
    ];
  }
  $element['selection'] = [
    '#type' => 'container',
    '#theme_wrappers' => [
      'container__media_library_widget_selection',
    ],
    '#attributes' => [
      'class' => [
        'js-media-library-selection',
      ],
    ],
  ];
  foreach ($referenced_entities as $delta => $media_item) {
    $element['selection'][$delta] = [
      '#theme' => 'media_library_item__widget',
      '#attributes' => [
        'class' => [
          'js-media-library-item',
        ],
        // Add the tabindex '-1' to allow the focus to be shifted to the next
        // media item when an item is removed. We set focus to the container
        // because we do not want to set focus to the remove button
        // automatically.
        // @see ::updateWidget()
        'tabindex' => '-1',
        // Add a data attribute containing the delta to allow us to easily
        // shift the focus to a specific media item.
        // @see ::updateWidget()
        'data-media-library-item-delta' => $delta,
      ],
      'remove_button' => [
        '#type' => 'submit',
        '#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix,
        '#value' => $this
          ->t('Remove'),
        '#media_id' => $media_item
          ->id(),
        '#attributes' => [
          'aria-label' => $this
            ->t('Remove @label', [
            '@label' => $media_item
              ->label(),
          ]),
        ],
        '#ajax' => [
          'callback' => [
            static::class,
            'updateWidget',
          ],
          'wrapper' => $wrapper_id,
          'progress' => [
            'type' => 'throbber',
            'message' => $this
              ->t('Removing @label.', [
              '@label' => $media_item
                ->label(),
            ]),
          ],
        ],
        '#submit' => [
          [
            static::class,
            'removeItem',
          ],
        ],
        // Prevent errors in other widgets from preventing removal.
        '#limit_validation_errors' => $limit_validation_errors,
      ],
      // @todo Make the view mode configurable in https://www.drupal.org/project/drupal/issues/2971209
      'rendered_entity' => $view_builder
        ->view($media_item, 'media_library'),
      'target_id' => [
        '#type' => 'hidden',
        '#value' => $media_item
          ->id(),
      ],
      // This hidden value can be toggled visible for accessibility.
      'weight' => [
        '#type' => 'number',
        '#theme' => 'input__number__media_library_item_weight',
        '#title' => $this
          ->t('Weight'),
        '#access' => $multiple_items,
        '#default_value' => $delta,
        '#attributes' => [
          'class' => [
            'js-media-library-item-weight',
          ],
        ],
      ],
    ];
  }
  $cardinality_unlimited = $element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED;
  $remaining = $element['#cardinality'] - count($referenced_entities);

  // Inform the user of how many items are remaining.
  if (!$cardinality_unlimited) {
    if ($remaining) {
      $cardinality_message = $this
        ->formatPlural($remaining, 'One media item remaining.', '@count media items remaining.');
    }
    else {
      $cardinality_message = $this
        ->t('The maximum number of media items have been selected.');
    }

    // Add a line break between the field message and the cardinality message.
    if (!empty($element['#description'])) {
      $element['#description'] .= '<br />';
    }
    $element['#description'] .= $cardinality_message;
  }

  // Create a new media library URL with the correct state parameters.
  $selected_type_id = reset($allowed_media_type_ids);
  $remaining = $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining;

  // This particular media library opener needs some extra metadata for its
  // \Drupal\media_library\MediaLibraryOpenerInterface::getSelectionResponse()
  // to be able to target the element whose 'data-media-library-widget-value'
  // attribute is the same as $field_widget_id. The entity ID, entity type ID,
  // bundle, field name are used for access checking.
  $entity = $items
    ->getEntity();
  $opener_parameters = [
    'field_widget_id' => $field_widget_id,
    'entity_type_id' => $entity
      ->getEntityTypeId(),
    'bundle' => $entity
      ->bundle(),
    'field_name' => $field_name,
  ];

  // Only add the entity ID when we actually have one. The entity ID needs to
  // be a string to ensure that the media library state generates its
  // tamper-proof hash in a consistent way.
  if (!$entity
    ->isNew()) {
    $opener_parameters['entity_id'] = (string) $entity
      ->id();
    if ($entity
      ->getEntityType()
      ->isRevisionable()) {
      $opener_parameters['revision_id'] = (string) $entity
        ->getRevisionId();
    }
  }
  $state = MediaLibraryState::create('media_library.opener.field_widget', $allowed_media_type_ids, $selected_type_id, $remaining, $opener_parameters);

  // Add a button that will load the Media library in a modal using AJAX.
  $element['open_button'] = [
    '#type' => 'button',
    '#value' => $this
      ->t('Add media'),
    '#name' => $field_name . '-media-library-open-button' . $id_suffix,
    '#attributes' => [
      'class' => [
        'js-media-library-open-button',
      ],
    ],
    '#media_library_state' => $state,
    '#ajax' => [
      'callback' => [
        static::class,
        'openMediaLibrary',
      ],
      'progress' => [
        'type' => 'throbber',
        'message' => $this
          ->t('Opening media library.'),
      ],
      // The AJAX system automatically moves focus to the first tabbable
      // element of the modal, so we need to disable refocus on the button.
      'disable-refocus' => TRUE,
    ],
    // Allow the media library to be opened even if there are form errors.
    '#limit_validation_errors' => [],
  ];

  // When the user returns from the modal to the widget, we want to shift the
  // focus back to the open button. If the user is not allowed to add more
  // items, the button needs to be disabled. Since we can't shift the focus to
  // disabled elements, the focus is set back to the open button via
  // JavaScript by adding the 'data-disabled-focus' attribute.
  // @see Drupal.behaviors.MediaLibraryWidgetDisableButton
  if (!$cardinality_unlimited && $remaining === 0) {
    $triggering_element = $form_state
      ->getTriggeringElement();
    if ($triggering_element && ($trigger_parents = $triggering_element['#array_parents']) && end($trigger_parents) === 'media_library_update_widget') {

      // The widget is being rebuilt from a selection change.
      $element['open_button']['#attributes']['data-disabled-focus'] = 'true';
      $element['open_button']['#attributes']['class'][] = 'visually-hidden';
    }
    else {

      // The widget is being built without a selection change, so we can just
      // set the item to disabled now, there is no need to set the focus
      // first.
      $element['open_button']['#disabled'] = TRUE;
      $element['open_button']['#attributes']['class'][] = 'visually-hidden';
    }
  }

  // This hidden field and button are used to add new items to the widget.
  $element['media_library_selection'] = [
    '#type' => 'hidden',
    '#attributes' => [
      // This is used to pass the selection from the modal to the widget.
      'data-media-library-widget-value' => $field_widget_id,
    ],
  ];

  // When a selection is made this hidden button is pressed to add new media
  // items based on the "media_library_selection" value.
  $element['media_library_update_widget'] = [
    '#type' => 'submit',
    '#value' => $this
      ->t('Update widget'),
    '#name' => $field_name . '-media-library-update' . $id_suffix,
    '#ajax' => [
      'callback' => [
        static::class,
        'updateWidget',
      ],
      'wrapper' => $wrapper_id,
      'progress' => [
        'type' => 'throbber',
        'message' => $this
          ->t('Adding selection.'),
      ],
    ],
    '#attributes' => [
      'data-media-library-widget-update' => $field_widget_id,
      'class' => [
        'js-hide',
      ],
    ],
    '#validate' => [
      [
        static::class,
        'validateItems',
      ],
    ],
    '#submit' => [
      [
        static::class,
        'addItems',
      ],
    ],
    // We need to prevent the widget from being validated when no media items
    // are selected. When a media field is added in a subform, entity
    // validation is triggered in EntityFormDisplay::validateFormValues().
    // Since the media item is not added to the form yet, this triggers errors
    // for required media fields.
    '#limit_validation_errors' => !empty($referenced_entities) ? $limit_validation_errors : [],
  ];
  return $element;
}