You are here

EditorMediaDialog.php in Drupal 8

Namespace

Drupal\media\Form

File

core/modules/media/src/Form/EditorMediaDialog.php
View source
<?php

namespace Drupal\media\Form;

use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\EditorInterface;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\editor\Ajax\EditorDialogSave;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\filter\Plugin\FilterInterface;
use Drupal\image\Plugin\Field\FieldType\ImageItem;
use Drupal\media\MediaInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;

/**
 * Provides a media embed dialog for text editors.
 *
 * Depending on the configuration of the filters associated with the text
 * editor, this dialog allows users to set the alt text, alignment, and
 * captioning status for embedded media items.
 *
 * @internal
 *   This is an internal part of the media system in Drupal core and may be
 *   subject to change in minor releases. This class should not be
 *   instantiated or extended by external code.
 */
class EditorMediaDialog extends FormBase {

  /**
   * The entity repository.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected $entityRepository;

  /**
   * The entity display repository.
   *
   * @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
   */
  protected $entityDisplayRepository;

  /**
   * Constructs a EditorMediaDialog object.
   *
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   *   The entity repository.
   * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
   *   The entity display repository.
   */
  public function __construct(EntityRepositoryInterface $entity_repository, EntityDisplayRepositoryInterface $entity_display_repository) {
    $this->entityRepository = $entity_repository;
    $this->entityDisplayRepository = $entity_display_repository;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container
      ->get('entity.repository'), $container
      ->get('entity_display.repository'));
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId() {
    return 'editor_media_dialog';
  }

  /**
   * {@inheritdoc}
   *
   * @param \Drupal\editor\EditorInterface $editor
   *   The text editor to which this dialog corresponds.
   */
  public function buildForm(array $form, FormStateInterface $form_state, EditorInterface $editor = NULL) {

    // This form is special, in that the default values do not come from the
    // server side, but from the client side, from a text editor. We must cache
    // this data in form state, because when the form is rebuilt, we will be
    // receiving values from the form, instead of the values from the text
    // editor. If we don't cache it, this data will be lost. By convention,
    // the data that the text editor sends to any dialog is in the
    // 'editor_object' key.
    if (isset($form_state
      ->getUserInput()['editor_object'])) {
      $editor_object = $form_state
        ->getUserInput()['editor_object'];

      // The data that the text editor sends to any dialog is in
      // the 'editor_object' key.
      // @see core/modules/ckeditor/js/ckeditor.es6.js
      $media_embed_element = $editor_object['attributes'];
      $form_state
        ->set('media_embed_element', $media_embed_element);
      $has_caption = $editor_object['hasCaption'];
      $form_state
        ->set('hasCaption', $has_caption)
        ->setCached(TRUE);
    }
    else {

      // Retrieve the user input from form state.
      $media_embed_element = $form_state
        ->get('media_embed_element');
      $has_caption = $form_state
        ->get('hasCaption');
    }
    $form['#tree'] = TRUE;
    $form['#attached']['library'][] = 'editor/drupal.editor.dialog';
    $form['#prefix'] = '<div id="editor-media-dialog-form">';
    $form['#suffix'] = '</div>';
    $filters = $editor
      ->getFilterFormat()
      ->filters();
    $filter_html = $filters
      ->get('filter_html');
    $filter_align = $filters
      ->get('filter_align');
    $filter_caption = $filters
      ->get('filter_caption');
    $media_embed_filter = $filters
      ->get('media_embed');
    $allowed_attributes = [];
    if ($filter_html->status) {
      $restrictions = $filter_html
        ->getHTMLRestrictions();
      $allowed_attributes = $restrictions['allowed']['drupal-media'];
    }
    $media = $this->entityRepository
      ->loadEntityByUuid('media', $media_embed_element['data-entity-uuid']);
    if ($image_field_name = $this
      ->getMediaImageSourceFieldName($media)) {

      // We'll want the alt text from the same language as the host.
      if (!empty($editor_object['hostEntityLangcode']) && $media
        ->hasTranslation($editor_object['hostEntityLangcode'])) {
        $media = $media
          ->getTranslation($editor_object['hostEntityLangcode']);
      }
      $settings = $media->{$image_field_name}
        ->getItemDefinition()
        ->getSettings();
      $alt = isset($media_embed_element['alt']) ? $media_embed_element['alt'] : NULL;
      $form['alt'] = [
        '#type' => 'textfield',
        '#title' => $this
          ->t('Alternate text'),
        '#default_value' => $alt,
        '#description' => $this
          ->t('Short description of the image used by screen readers and displayed when the image is not loaded. This is important for accessibility.'),
        '#required_error' => $this
          ->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'),
        '#maxlength' => 2048,
        '#placeholder' => $media->{$image_field_name}->alt,
        '#parents' => [
          'attributes',
          'alt',
        ],
        '#access' => !empty($settings['alt_field']) && ($filter_html->status === FALSE || !empty($allowed_attributes['alt'])),
      ];
    }

    // When Drupal core's filter_align is being used, the text editor offers the
    // ability to change the alignment.
    $form['align'] = [
      '#title' => $this
        ->t('Align'),
      '#type' => 'radios',
      '#options' => [
        'none' => $this
          ->t('None'),
        'left' => $this
          ->t('Left'),
        'center' => $this
          ->t('Center'),
        'right' => $this
          ->t('Right'),
      ],
      '#default_value' => empty($media_embed_element['data-align']) ? 'none' : $media_embed_element['data-align'],
      '#attributes' => [
        'class' => [
          'container-inline',
        ],
      ],
      '#parents' => [
        'attributes',
        'data-align',
      ],
      '#access' => $filter_align->status && ($filter_html->status === FALSE || !empty($allowed_attributes['data-align'])),
    ];

    // When Drupal core's filter_caption is being used, the text editor offers
    // the ability to in-place edit the media's caption: show a toggle.
    $form['caption'] = [
      '#title' => $this
        ->t('Caption'),
      '#type' => 'checkbox',
      '#default_value' => $has_caption === 'true',
      '#parents' => [
        'hasCaption',
      ],
      '#access' => $filter_caption->status && ($filter_html->status === FALSE || !empty($allowed_attributes['data-caption'])),
    ];
    $view_mode_options = array_intersect_key($this->entityDisplayRepository
      ->getViewModeOptions('media'), $media_embed_filter->settings['allowed_view_modes']);
    $default_view_mode = static::getViewModeDefaultValue($view_mode_options, $media_embed_filter, $media_embed_element['data-view-mode'] ?? NULL);
    $form['view_mode'] = [
      '#title' => $this
        ->t("Display"),
      '#type' => 'select',
      '#options' => $view_mode_options,
      '#default_value' => $default_view_mode,
      '#parents' => [
        'attributes',
        'data-view-mode',
      ],
      '#access' => count($view_mode_options) >= 2,
    ];

    // Store the default from the MediaEmbed filter, so that if the selected
    // view mode matches the default, we can drop the 'data-view-mode'
    // attribute.
    $form_state
      ->set('filter_default_view_mode', $media_embed_filter->settings['default_view_mode']);
    if ((empty($form['alt']) || $form['alt']['#access'] === FALSE) && $form['align']['#access'] === FALSE && $form['caption']['#access'] === FALSE && $form['view_mode']['#access'] === FALSE) {
      $format = $editor
        ->getFilterFormat();
      $warning = $this
        ->t('There is nothing to configure for this media.');
      $form['no_access_notice'] = [
        '#markup' => $warning,
      ];
      if ($format
        ->access('update')) {
        $tparams = [
          '@warning' => $warning,
          '@edit_url' => $format
            ->toUrl('edit-form')
            ->toString(),
          '%format' => $format
            ->label(),
        ];
        $form['no_access_notice']['#markup'] = $this
          ->t('@warning <a href="@edit_url">Edit the text format %format</a> to modify the attributes that can be overridden.', $tparams);
      }
    }
    $form['actions'] = [
      '#type' => 'actions',
    ];
    $form['actions']['save_modal'] = [
      '#type' => 'submit',
      '#value' => $this
        ->t('Save'),
      // No regular submit-handler. This form only works via JavaScript.
      '#submit' => [],
      '#ajax' => [
        'callback' => '::submitForm',
        'event' => 'click',
      ],
      // Prevent this hidden element from being tabbable.
      '#attributes' => [
        'tabindex' => -1,
      ],
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    $response = new AjaxResponse();

    // When the `alt` attribute is set to two double quotes, transform it to the
    // empty string: two double quotes signify "empty alt attribute". See above.
    if (trim($form_state
      ->getValue([
      'attributes',
      'alt',
    ], '')) === '""') {
      $form_state
        ->setValue([
        'attributes',
        'alt',
      ], '""');
    }

    // The `alt` attribute is optional: if it isn't set, the default value
    // simply will not be overridden. It's important to set it to FALSE
    // instead of unsetting the value.  This way we explicitly inform
    // the client side about the new value.
    if ($form_state
      ->hasValue([
      'attributes',
      'alt',
    ]) && trim($form_state
      ->getValue([
      'attributes',
      'alt',
    ])) === '') {
      $form_state
        ->setValue([
        'attributes',
        'alt',
      ], FALSE);
    }

    // If the selected view mode matches the default on the filter, remove the
    // attribute.
    if (!empty($form_state
      ->get('filter_default_view_mode')) && $form_state
      ->getValue([
      'attributes',
      'data-view-mode',
    ]) === $form_state
      ->get('filter_default_view_mode')) {
      $form_state
        ->setValue([
        'attributes',
        'data-view-mode',
      ], FALSE);
    }
    if ($form_state
      ->getErrors()) {
      unset($form['#prefix'], $form['#suffix']);
      $form['status_messages'] = [
        '#type' => 'status_messages',
        '#weight' => -10,
      ];
      $response
        ->addCommand(new HtmlCommand('#editor-media-dialog-form', $form));
    }
    else {

      // Only send back the relevant values.
      $values = [
        'hasCaption' => $form_state
          ->getValue('hasCaption'),
        'attributes' => $form_state
          ->getValue('attributes'),
      ];
      $response
        ->addCommand(new EditorDialogSave($values));
      $response
        ->addCommand(new CloseModalDialogCommand());
    }
    return $response;
  }

  /**
   * Gets the default value for the view mode form element.
   *
   * @param array $view_mode_options
   *   The array of options for the view mode form element.
   * @param \Drupal\filter\Plugin\FilterInterface $media_embed_filter
   *   The media embed filter.
   * @param string $media_element_view_mode_attribute
   *   The data-view-mode attribute on the <drupal-media> element.
   *
   * @return string|null
   *   The default value for the view mode form element.
   */
  public static function getViewModeDefaultValue(array $view_mode_options, FilterInterface $media_embed_filter, $media_element_view_mode_attribute) {

    // The select element won't display without at least two options, so if
    // that's the case, just return NULL.
    if (count($view_mode_options) < 2) {
      return NULL;
    }
    $filter_default_view_mode = $media_embed_filter->settings['default_view_mode'];

    // If the current media embed ($media_embed_element) has a set view mode,
    // we want to use that as the default in the select form element,
    // otherwise we'll want to use the default for all embedded media.
    if (!empty($media_element_view_mode_attribute) && array_key_exists($media_element_view_mode_attribute, $view_mode_options)) {
      return $media_element_view_mode_attribute;
    }
    elseif (array_key_exists($filter_default_view_mode, $view_mode_options)) {
      return $filter_default_view_mode;
    }
    return NULL;
  }

  /**
   * Gets the name of an image media item's source field.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media item being embedded.
   *
   * @return string|null
   *   The name of the image source field configured for the media item, or
   *   NULL if the source field is not an image field.
   */
  protected function getMediaImageSourceFieldName(MediaInterface $media) {
    $field_definition = $media
      ->getSource()
      ->getSourceFieldDefinition($media->bundle->entity);
    $item_class = $field_definition
      ->getItemDefinition()
      ->getClass();
    if (is_a($item_class, ImageItem::class, TRUE)) {
      return $field_definition
        ->getName();
    }
    return NULL;
  }

}

Classes

Namesort descending Description
EditorMediaDialog Provides a media embed dialog for text editors.