diff --git a/core/modules/media_library/js/media_library.widget.es6.js b/core/modules/media_library/js/media_library.widget.es6.js index b42ef705..80bacbd1 100644 --- a/core/modules/media_library/js/media_library.widget.es6.js +++ b/core/modules/media_library/js/media_library.widget.es6.js @@ -68,6 +68,25 @@ }, }; + /** + * Allow users to edit media library items inside a modal. + */ + Drupal.behaviors.MediaLibraryWidgetEditItem = { + attach: function attach() { + $('.media-library-widget .edit-media') + .once('media-library-edit') + .on('click', function () { + // Remove any "selected-media" classes. + $(this) + .closest('.media-library-selection') + .find('.selected-media') + .removeClass('selected-media'); + // Mark the media item as selected to render it properly when submitting an ajax media edit request. + $(this).parent().find('article').addClass('selected-media'); + }); + }, + }; + /** * Disable the open button when the user is not allowed to add more items. * diff --git a/core/modules/media_library/js/media_library.widget.js b/core/modules/media_library/js/media_library.widget.js index 0fe2475e..3b321297 100644 --- a/core/modules/media_library/js/media_library.widget.js +++ b/core/modules/media_library/js/media_library.widget.js @@ -35,6 +35,14 @@ $('.js-media-library-item-weight', context).once('media-library-toggle').parent().hide(); } }; + Drupal.behaviors.MediaLibraryWidgetEditItem = { + attach: function attach() { + $('.media-library-widget .edit-media').once('media-library-edit').on('click', function () { + $(this).closest('.media-library-selection').find('.selected-media').removeClass('selected-media'); + $(this).parent().find('article').addClass('selected-media'); + }); + } + }; Drupal.behaviors.MediaLibraryWidgetDisableButton = { attach: function attach(context) { $('.js-media-library-open-button[data-disabled-focus="true"]', context).once('media-library-disable').each(function () { diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module index 1b6beb87..6bc4b269 100644 --- a/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -5,11 +5,16 @@ * Contains hook implementations for the media_library module. */ +use Drupal\Component\Utility\Html; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Access\AccessResult; +use Drupal\Core\Ajax\AjaxResponse; +use Drupal\Core\Ajax\CloseModalDialogCommand; +use Drupal\Core\Ajax\ReplaceCommand; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\Entity\EntityViewDisplay; +use Drupal\Core\EventSubscriber\MainContentViewSubscriber; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Render\Element; @@ -28,6 +33,7 @@ use Drupal\views\Plugin\views\cache\CachePluginBase; use Drupal\views\ViewExecutable; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\Component\Serialization\Json; +use Drupal\Core\Messenger\MessengerInterface; /** * Implements hook_help(). @@ -224,6 +230,78 @@ function media_library_views_post_render(ViewExecutable $view, &$output, CachePl } } +/** + * Implements hook_form_FORM_ID_alter() for media_form forms. + */ +function media_library_form_media_form_alter(array &$form, FormStateInterface $form_state) { + // Make the media edit form work inside modal dialogs. + $form_object = $form_state->getFormObject(); + if (is_object($form_object) && $form_object->getOperation() === 'edit') { + $wrapper_format = \Drupal::request()->query->get(MainContentViewSubscriber::WRAPPER_FORMAT); + if ($wrapper_format === 'drupal_modal' || ($wrapper_format === 'drupal_ajax')) { + foreach (Element::children($form['actions']) as $key) { + if ($key === 'submit') { + // The default name is 'op', but we change it on purpose so that it is + // easier to detect our modified AJAX call above. + $form['actions'][$key]['#name'] = 'media_library_ajax_submit'; + $form['actions'][$key]['#ajax'] = [ + 'callback' => 'media_library_media_form_ajax_submit', + ]; + + // Prevent the form that opened the modal dialog to refocus to the + // 'Save' button at the bottom of the form. + $form['actions'][$key]['#attributes']['data-disable-refocus'] = 'true'; + + // Reattach dialog AJAX library. + $form['#attached']['library'][] = 'core/drupal.dialog.ajax'; + + // The data-drupal-selector needs to be the same between the various + // AJAX requests. A bug in \Drupal\Core\Form\FormBuilder prevents that + // from happening unless $form['#id'] is also the same. Normally, #id + // is set to a unique HTML ID via Html::getUniqueId(), but here we + // bypass that in order to work around the data-drupal-selector bug. + // This is okay so long as we assume that this form only ever occurs + // once on a page. + // @todo: Remove once https://www.drupal.org/node/2897377 is fixed. + $form['#id'] = Html::getId($form_state->getBuildInfo()['form_id']); + } + else { + // We just want to allow the save action for now. Just hide other + // actions like the delete action. + $form['actions'][$key]['#access'] = FALSE; + } + } + } + } +} + +/** + * AJAX callback for media edit form when form is shown inside modal dialog. + * + * @ingroup form + */ +function media_library_media_form_ajax_submit(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + if (!$form_state->hasAnyErrors()) { + $media = $form_state->getFormObject()->getEntity(); + $render_array = \Drupal::entityTypeManager()->getViewBuilder('media')->view($media, 'media_library'); + $html = \Drupal::service('renderer')->render($render_array); + $response->addCommand(new CloseModalDialogCommand()); + $response->addCommand(new ReplaceCommand('.selected-media', $html)); + + // Remove status messages when editing medias. + \Drupal::messenger()->deleteByType(MessengerInterface::TYPE_STATUS); + } + else { + $form['status_messages'] = [ + '#type' => 'status_messages', + '#weight' => -1000, + ]; + $response->addCommand(new ReplaceCommand('[data-drupal-selector="' . $form['#attributes']['data-drupal-selector'] . '"]', $form)); + } + return $response; +} + /** * Implements hook_preprocess_media(). */ diff --git a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php index 1d890c97..60f72e0c 100644 --- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php @@ -362,6 +362,52 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface ]; } + $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'] .= '
'; + } + $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); + $element['selection'] = [ '#type' => 'container', '#theme_wrappers' => [ @@ -396,6 +442,7 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface '#type' => 'submit', '#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix, '#value' => $this->t('Remove'), + '#weight' => -10, '#media_id' => $media_item->id(), '#attributes' => [ 'aria-label' => $this->t('Remove @label', ['@label' => $media_item->label()]), @@ -412,6 +459,19 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface // Prevent errors in other widgets from preventing removal. '#limit_validation_errors' => $limit_validation_errors, ], + 'edit_button' => [ + '#type' => 'button', + '#value' => $this->t('Edit'), + '#weight' => -5, + '#access' => $media_item->access('update'), + '#attributes' => [ + 'type' => 'button', + 'class' => ['edit-media', 'use-ajax'], + 'href' => $media_item->toUrl('edit-form', ['query' => $state->all()])->toString(), + 'data-dialog-type' => 'modal', + 'data-dialog-options' => '{"width":"80%"}', + ], + ], // @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' => [ @@ -434,52 +494,6 @@ class MediaLibraryWidget extends WidgetBase implements TrustedCallbackInterface ]; } - $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'] .= '
'; - } - $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',