diff -u b/core/modules/media_library/config/install/views.view.media_library.yml b/core/modules/media_library/config/install/views.view.media_library.yml --- b/core/modules/media_library/config/install/views.view.media_library.yml +++ b/core/modules/media_library/config/install/views.view.media_library.yml @@ -8,6 +8,7 @@ - media_library module: - media + - media_library - user id: media_library label: 'Media library' @@ -416,6 +417,166 @@ footer: { } empty: { } relationships: { } + display_extenders: { } + use_ajax: true + css_class: media-library-view + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - 'url.query_args:sort_by' + - user.permissions + tags: { } + page: + display_plugin: page + id: page + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: admin/content/media + menu: + type: tab + title: Media + description: 'Allows users to browse and administer media items' + expanded: false + parent: system.admin_content + weight: 5 + context: '0' + menu_name: admin + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - 'url.query_args:sort_by' + - user.permissions + tags: { } + widget: + display_plugin: page + id: widget + display_title: Widget + position: 2 + display_options: + display_extenders: { } + path: admin/content/media-widget + fields: + rendered_entity: + id: rendered_entity + table: media + field: rendered_entity + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: media-library-item__content + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + view_mode: media_library + entity_type: media + plugin_id: rendered_entity + media_library_select_form: + id: media_library_select_form + table: media + field: media_library_select_form + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: click-to-select-checkbox + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + entity_type: media + plugin_id: media_library_select_form + defaults: + fields: false + display_description: '' + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - 'url.query_args:sort_by' + - user.permissions + tags: { } arguments: bundle: id: bundle @@ -464,35 +624,0 @@ - cache_metadata: - max-age: 0 - contexts: - - 'languages:language_interface' - - url - - url.query_args - - 'url.query_args:sort_by' - - user.permissions - tags: { } - page: - display_plugin: page - id: page - display_title: Page - position: 1 - display_options: - display_extenders: { } - path: admin/content/media - menu: - type: tab - title: Media - description: 'Allows users to browse and administer media items' - expanded: false - parent: system.admin_content - weight: 5 - context: '0' - menu_name: admin - cache_metadata: - max-age: 0 - contexts: - - 'languages:language_interface' - - url - - url.query_args - - 'url.query_args:sort_by' - - user.permissions - tags: { } diff -u b/core/modules/media_library/css/media_library.css b/core/modules/media_library/css/media_library.css --- b/core/modules/media_library/css/media_library.css +++ b/core/modules/media_library/css/media_library.css @@ -2,7 +2,8 @@ * @file media_library.view.css */ -.media-library-view .view-content form { +.media-library-view .view-content form, +.media-library-selection { display: flex; flex-wrap: wrap; align-content: space-between; @@ -137,0 +139,50 @@ + +.media-library-selection { + margin-bottom: 1.5rem; +} + +.media-library-selection .media-library-item__preview { + cursor: move; +} + +.media-library-selection .media-library-item::after { + display: none; +} + +.media-library-item .media-library-item-remove { + position: absolute; + z-index: 1; + top: 0; + right: 0; + width: 24px; + height: 24px; + margin: 5px; + padding: 0; + background: url('../../../misc/icons/787878/ex.svg') #ffffff center no-repeat; + background-size: 16px 16px; + border: 2px solid #ccc; + border-radius: 20px; + color: transparent; + text-shadow: none; + transition: .2s border-color; +} + +.media-library-item .media-library-item-remove:hover, +.media-library-item .media-library-item-remove:focus, +.media-library-item .media-library-item-remove.button:disabled { + border-color: #40b6ff; + background: url('../../../misc/icons/787878/ex.svg') #ffffff center no-repeat; + background-size: 16px 16px; + color: transparent; + text-shadow: none; +} + +.media-library-toggle-weight { + position: absolute; + right: 5px; + top: 5px; +} + +.media-library-item .form-item { + margin: .75em; +} diff -u b/core/modules/media_library/media_library.libraries.yml b/core/modules/media_library/media_library.libraries.yml --- b/core/modules/media_library/media_library.libraries.yml +++ b/core/modules/media_library/media_library.libraries.yml @@ -23,0 +24,10 @@ + +widget: + version: VERSION + js: + js/media_library.widget.js: {} + dependencies: + - core/drupal + - core/jquery + - core/jquery.ui.sortable + - media_library/view diff -u b/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module --- b/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -41,6 +41,11 @@ function media_library_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) { if ($view->id() === 'media_library') { $output['#attached']['library'][] = 'media_library/view'; + // Ensure that the widget ID passed to the modal persists across rebuilds. + $request = \Drupal::request(); + if ($request->query->has('media_library_widget_id')) { + $output['#attached']['drupalSettings']['views']['ajax_path'] .= '?media_library_widget_id=' . $request->query->get('media_library_widget_id'); + } } } only in patch2: unchanged: --- /dev/null +++ b/core/modules/media_library/js/media_library.widget.es6.js @@ -0,0 +1,62 @@ +/** + * @file media_library.widget.js + */ + +(function ($, Drupal) { + + "use strict"; + + /** + * Provides custom JS behaviors related to the media library field widget. + */ + Drupal.behaviors.MediaLibraryWidget = { + attach: function (context) { + // Allow media items to be re-sorted with drag+drop in the widget. + $('.media-library-selection', context).once('media-library-sortable').sortable({ + tolerance: 'pointer', + helper: 'clone', + handle: '.media-library-item__preview', + stop: function(event, ui) { + // Update all the hidden "weight" fields. + $(event.target).children().each(function (index) { + $(this).find('.media-library-item-weight').val(index); + }); + } + }); + // Allow item order to be set without drag+drop for accessibility. + $('.media-library-toggle-weight', context).once('media-library-toggle') + .attr('title', Drupal.t('Re-order media by numerical weight instead of dragging.')) + .on('click', function (e) { + e.preventDefault(); + $('.media-library-item-weight').parent().toggle(); + }) + .text(Drupal.t('Toggle media item weights')); + $('.media-library-item-weight', context).once('media-library-toggle').parent().hide(); + // Warn users when clicking outgoing links from the library or widget. + $('.media-library-item a[href]', context).once('media-library-warn-link') + .on('click', function (e) { + const message = Drupal.t('Unsaved changes to the form will be lost. Are you sure you want to leave?'); + const confirmation = window.confirm(message); + if (!confirmation) { + e.preventDefault(); + } + }); + } + }; + + /** + * Ajax command to add items to the media widget. + * + * @param {Drupal.Ajax} ajax + * {@link Drupal.Ajax} object created by {@link Drupal.ajax}. + * @param {object} response + * JSON response from the Ajax request. + * @param {number} [status] + * XMLHttpRequest status. + */ + Drupal.AjaxCommands.prototype.addMediaToWidget = function (ajax, response, status) { + $(`[data-media-library-widget-value="${response.identifier}"]`).val(response.mids.join(',')); + $(`[data-media-library-widget-update="${response.identifier}"]`).trigger('mousedown'); + }; + +})(jQuery, Drupal); only in patch2: unchanged: --- /dev/null +++ b/core/modules/media_library/js/media_library.widget.js @@ -0,0 +1,45 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, Drupal) { + + "use strict"; + + Drupal.behaviors.MediaLibraryWidget = { + attach: function attach(context) { + $('.media-library-selection', context).once('media-library-sortable').sortable({ + tolerance: 'pointer', + helper: 'clone', + handle: '.media-library-item__preview', + stop: function stop(event, ui) { + $(event.target).children().each(function (index) { + $(this).find('.media-library-item-weight').val(index); + }); + } + }); + + $('.media-library-toggle-weight', context).once('media-library-toggle').attr('title', Drupal.t('Re-order media by numerical weight instead of dragging.')).on('click', function (e) { + e.preventDefault(); + $('.media-library-item-weight').parent().toggle(); + }).text(Drupal.t('Toggle media item weights')); + $('.media-library-item-weight', context).once('media-library-toggle').parent().hide(); + + $('.media-library-item a[href]', context).once('media-library-warn-link').on('click', function (e) { + var message = Drupal.t('Unsaved changes to the form will be lost. Are you sure you want to leave?'); + var confirmation = window.confirm(message); + if (!confirmation) { + e.preventDefault(); + } + }); + } + }; + + Drupal.AjaxCommands.prototype.addMediaToWidget = function (ajax, response, status) { + $('[data-media-library-widget-value="' + response.identifier + '"]').val(response.mids.join(',')); + $('[data-media-library-widget-update="' + response.identifier + '"]').trigger('mousedown'); + }; +})(jQuery, Drupal); \ No newline at end of file only in patch2: unchanged: --- /dev/null +++ b/core/modules/media_library/media_library.views.inc @@ -0,0 +1,22 @@ + t('Select media'), + 'help' => t('Provides a field for selecting media entities in our media library view'), + 'real field' => 'mid', + 'field' => [ + 'id' => 'media_library_select_form', + ], + ]; + return $data; +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media_library/src/Ajax/AddMediaToWidget.php @@ -0,0 +1,61 @@ +"] + * An input element that will store new MIDs. + * - [data-media-library-widget-update=""] + * An AJAX button that will be clicked to inform the widget of an update. + * + * @ingroup ajax + */ +class AddMediaToWidget implements CommandInterface { + + /** + * Media IDs to pass to a field widget. + * + * @var array + */ + protected $mids; + + /** + * An identifier for the field widget. + * + * @var string + */ + protected $identifier; + + /** + * AddMediaToWidget constructor. + * + * @param array $mids + * Media IDs to pass to a field widget. + * @param string $identifier + * An identifier for the field widget. + */ + public function __construct(array $mids, $identifier) { + $this->mids = $mids; + $this->identifier = $identifier; + } + + /** + * {@inheritdoc} + */ + public function render() { + return [ + 'command' => 'addMediaToWidget', + 'mids' => $this->mids, + 'identifier' => $this->identifier, + ]; + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php @@ -0,0 +1,443 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + return $field_definition->getSetting('target_type') === 'media'; + } + + /** + * {@inheritdoc} + */ + public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) { + $field_name = $this->fieldDefinition->getName(); + $parents = $form['#parents']; + + // Load the items for form rebuilds from the field state. + $field_state = static::getWidgetState($parents, $field_name, $form_state); + if ($field_state && isset($field_state['items'])) { + usort($field_state['items'], [SortArray::class, 'sortByWeightElement']); + $items->setValue($field_state['items']); + } + + $build = parent::form($items, $form, $form_state, $get_delta); + + return $build; + } + + /** + * {@inheritdoc} + */ + 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($this->getFieldSetting('target_type')); + $field_name = $this->fieldDefinition->getName(); + $parents = $form['#parents']; + $id_suffix = '-' . implode('-', $parents); + $wrapper_id = $field_name . '-media-library-wrapper' . $id_suffix; + + $settings = $this->getFieldSetting('handler_settings'); + $element += [ + '#type' => 'fieldset', + '#cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(), + '#target_bundles' => isset($settings['target_bundles']) ? $settings['target_bundles'] : FALSE, + 'selection' => [ + '#type' => 'container', + '#attributes' => [ + 'class' => 'media-library-selection', + ], + ], + '#attributes' => [ + 'id' => $wrapper_id, + 'class' => ['media-library-wrapper'], + ], + '#attached' => [ + 'library' => ['media_library/widget'], + ], + ]; + + if (empty($referenced_entities)) { + $element['empty_selection'] = [ + '#markup' => $this->t('

No media selected.

'), + ]; + } + else { + $element['weight_toggle'] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => $this->t('Toggle media item weights'), + '#attributes' => [ + 'class' => ['link', 'media-library-toggle-weight'], + ], + ]; + + foreach ($referenced_entities as $delta => $media_item) { + $element['selection'][$delta] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => ['media-library-item', 'media-library-item-in-widget'], + ], + 'preview' => [ + '#type' => 'container', + 'view' => $view_builder->view($media_item, 'media_library'), + 'remove_button' => [ + '#type' => 'submit', + '#media_library_remove_delta' => $delta, + '#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix, + '#value' => $this->t('Remove'), + '#attributes' => [ + 'class' => ['media-library-item-remove'], + ], + '#ajax' => [ + 'callback' => [static::class, 'updateWidget'], + 'wrapper' => $wrapper_id, + ], + '#submit' => [[static::class, 'removeItem']], + '#limit_validation_errors' => [array_merge($form['#parents'], [$field_name])], + ], + ], + 'target_id' => [ + '#type' => 'hidden', + '#value' => $media_item->id(), + ], + // This hidden value can be toggled visible for accessibility. + 'weight' => [ + '#type' => 'textfield', + '#title' => $this->t('Weight'), + '#default_value' => $delta, + '#attributes' => [ + 'class' => ['media-library-item-weight'], + ], + ], + ]; + } + } + + // @todo Contextual filters only work the first time the modal is loaded. + // Investigate and file an issue for Views if possible. + $element['media_library_open_container'] = [ + '#type' => 'container', + 'media_library_open_button' => [ + '#type' => 'link', + '#title' => $this->t('Add Media'), + '#name' => $field_name . '-media-library-open-button' . $id_suffix, + // @todo Make the View configurable. + '#url' => Url::fromRoute('view.media_library.widget', [], [ + 'query' => ['media_library_widget_id' => $field_name . $id_suffix], + ]), + '#attributes' => [ + 'class' => ['button', 'use-ajax', 'media-library-open-button'], + 'data-dialog-type' => 'modal', + 'data-dialog-options' => json_encode([ + 'dialogClass' => 'media-library-widget-modal', + 'height' => '75%', + 'width' => '75%', + 'title' => $this->t('Media library'), + ]), + ], + '#limit_validation_errors' => [array_merge($parents, [$field_name])], + ], + ]; + + // Add a hidden textfield to the form, which is filled by returning the + // \Drupal\media_library\Ajax\AddMediaToWidget command. + $element['media_library_selection'] = [ + '#type' => 'textfield', + '#attributes' => [ + 'data-media-library-widget-value' => $field_name . $id_suffix, + 'class' => ['visually-hidden'], + ], + ]; + + // This hidden button can be pressed to update the widget based on newly + // selected items. + $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, + ], + '#attributes' => [ + 'data-media-library-widget-update' => $field_name . $id_suffix, + 'class' => ['visually-hidden', 'media-library-update'], + ], + '#validate' => [[static::class, 'validateItems']], + '#submit' => [[static::class, 'updateItems']], + '#limit_validation_errors' => [array_merge($parents, [$field_name])], + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) { + return isset($element['target_id']) ? $element['target_id'] : FALSE; + } + + /** + * {@inheritdoc} + */ + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { + if (isset($values['selection'])) { + usort($values['selection'], [SortArray::class, 'sortByWeightElement']); + return $values['selection']; + } + else { + return []; + } + } + + /** + * AJAX callback to update the widget when the selection changes. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * An array representing the updated widget. + */ + public static function updateWidget(array $form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + $length = isset($triggering_element['#media_library_remove_delta']) ? -4 : -1; + $parents = $triggering_element['#array_parents']; + $parents = array_slice($parents, 0, $length); + $element = NestedArray::getValue($form, $parents); + // Always clear the textfield selection to prevent duplicate additions. + $element['media_library_selection']['#value'] = ''; + return $element; + } + + /** + * Submit callback for remove buttons. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function removeItem(array $form, FormStateInterface $form_state) { + $triggering_element = $form_state->getTriggeringElement(); + + $parents = array_slice($triggering_element['#array_parents'], 0, -4); + $element = NestedArray::getValue($form, $parents); + $delta = $triggering_element['#media_library_remove_delta']; + + // Find and remove correct entity. + $path = $element['#parents']; + $values = NestedArray::getValue($form_state->getValues(), $path); + $field_state = static::getFieldState($element, $form_state); + if (isset($values['selection'])) { + if (isset($values['selection'][$delta])) { + array_splice($values['selection'], $delta, 1); + $field_state['items'] = $values['selection']; + $field_state['items_count'] = count($field_state['items']); + } + } + static::setFieldState($element, $form_state, $field_state); + + $form_state->setRebuild(); + } + + /** + * Validates that newly selected items can be added to the widget. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function validateItems(array $form, FormStateInterface $form_state) { + $button = $form_state->getTriggeringElement(); + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1)); + + $field_state = self::getFieldState($element, $form_state); + $media = self::getNewMediaItems($element, $form_state); + if (!empty($media)) { + $cardinality_unlimited = ($element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + $selection = $field_state['items_count'] + count($media); + if (!$cardinality_unlimited && ($selection > $element['#cardinality'])) { + $form_state->setError($element, t('More items have been selected than this field allows (max @count).', [ + '@count' => $element['#cardinality'], + ])); + } + foreach ($media as $media_item) { + if ($element['#target_bundles'] && !in_array($media_item->bundle(), $element['#target_bundles'], TRUE)) { + $form_state->setError($element, t('The media item "@label" is not of an accepted type.', [ + '@label' => $media_item->label(), + ])); + } + } + } + } + + /** + * Updates the field state based on the form state and sets the form rebuild. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + */ + public static function updateItems(array $form, FormStateInterface $form_state) { + $button = $form_state->getTriggeringElement(); + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1)); + + $field_state = self::getFieldState($element, $form_state); + + $media = self::getNewMediaItems($element, $form_state); + $field_state['items'] = isset($field_state['items']) ? $field_state['items'] : []; + $weight = count($field_state['items']); + foreach ($media as $media_item) { + if ($media && $media_item->access('view')) { + $field_state['items'][] = [ + 'target_id' => $media_item->id(), + 'weight' => $weight++, + ]; + } + } + + $field_state['items_count'] = count($field_state['items']); + static::setFieldState($element, $form_state, $field_state); + + $form_state->setRebuild(); + } + + /** + * Gets newly selected media items. + * + * @param array $element + * The wrapping element for this widget. + * @param FormStateInterface $form_state + * The current state of the form. + * @return array|\Drupal\media\MediaInterface[] + * An array of selected media items. + */ + public static function getNewMediaItems(array $element, FormStateInterface $form_state) { + // Get the new media ids passed to our hidden button. + $values = $form_state->getValues(); + $path = $element['#parents']; + $value = NestedArray::getValue($values, $path); + + $media = []; + if (!empty($value['media_library_selection'])) { + $ids = explode(',', $value['media_library_selection']); + /** @var \Drupal\media\MediaInterface[] $media */ + $media = Media::loadMultiple($ids); + } + return $media; + } + + /** + * Gets the field state for the widget. + * + * @param array $element + * The wrapping element for this widget. + * @param FormStateInterface $form_state + * The current state of the form. + * @return array|mixed + * The field state. + */ + public static function getFieldState(array $element, FormStateInterface $form_state) { + $field_name = $element['#field_name']; + $parents = $element['#field_parents']; + return static::getWidgetState($parents, $field_name, $form_state); + } + + /** + * Sets the field state for the widget. + * + * @param array $element + * The wrapping element for this widget. + * @param FormStateInterface $form_state + * The current state of the form. + * @param array $field_state + * The field state to set. + */ + public static function setFieldState(array $element, FormStateInterface $form_state, array $field_state) { + $field_name = $element['#field_name']; + $parents = $element['#field_parents']; + static::setWidgetState($parents, $field_name, $form_state, $field_state); + } + +} only in patch2: unchanged: --- /dev/null +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php @@ -0,0 +1,129 @@ +options['id'] . '--' . $row->index . '-->'; + } + + /** + * {@inheritdoc} + */ + public function render(ResultRow $values) { + return ViewsRenderPipelineMarkup::create($this->getValue($values)); + } + + /** + * Form constructor for the media library select form. + * + * @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. + */ + public function viewsForm(array &$form, FormStateInterface $form_state) { + // Only add the bulk form options and buttons if there are results. + if (!empty($this->view->result)) { + // Render checkboxes for all rows. + $form[$this->options['id']]['#tree'] = TRUE; + foreach ($this->view->result as $row_index => $row) { + $entity = $this->getEntity($row); + $form[$this->options['id']][$row_index] = [ + '#type' => 'checkbox', + '#title' => $this->t('Select this item'), + '#title_display' => 'invisible', + '#return_value' => $entity->getEntityTypeId() . ':' . $entity->id(), + ]; + } + + // @todo Remove in https://www.drupal.org/project/drupal/issues/2504115 + $url = parse_url($form['#action'], PHP_URL_PATH); + + $form['actions']['submit']['#ajax'] = [ + 'url' => Url::fromUserInput($url), + 'options' => [ + 'query' => \Drupal::request()->query->all() + [ + FormBuilderInterface::AJAX_FORM_REQUEST => TRUE, + ], + ], + 'callback' => [static::class, 'updateWidget'], + ]; + + $form['actions']['submit']['#value'] = $this->t('Select media'); + $form['actions']['submit']['#field_id'] = $this->options['id']; + } + } + + /** + * Submit handler for the media library select form. + * + * @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. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * A command to send the selection to the current field widget. + * + */ + public static function updateWidget(array &$form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $widget_id = \Drupal::request()->query->get('media_library_widget_id'); + if ($widget_id && is_string($widget_id)) { + $field_id = $form_state->getTriggeringElement()['#field_id']; + $selected = array_values(array_filter($form_state->getValue($field_id, []))); + $mids = str_replace('media:', '', $selected); + $response->addCommand(new AddMediaToWidget($mids, $widget_id)); + $response->addCommand(new CloseDialogCommand()); + } + return $response; + } + + /** + * Returns the message to be displayed when there are no selected items. + * + * @return string + * Message displayed when no items are selected. + */ + protected function emptySelectedMessage() { + return $this->t('No items selected.'); + } + + /** + * {@inheritdoc} + */ + public function viewsFormValidate(array &$form, FormStateInterface $form_state) { + $selected = array_filter($form_state->getValue($this->options['id'])); + if (empty($selected)) { + $form_state->setErrorByName('', $this->emptySelectedMessage()); + } + } + + /** + * {@inheritdoc} + */ + public function clickSortable() { + return FALSE; + } + +}