diff --git a/core/modules/media_library/config/schema/media_library.schema.yml b/core/modules/media_library/config/schema/media_library.schema.yml index 5a6813995b..ce46c91e74 100644 --- a/core/modules/media_library/config/schema/media_library.schema.yml +++ b/core/modules/media_library/config/schema/media_library.schema.yml @@ -4,11 +4,7 @@ field.widget.settings.media_library_widget: mapping: media_types: type: sequence - label: 'Media types display order' + label: 'Media types tab order' sequence: - type: mapping - label: 'Media type weight' - mapping: - weight: - type: integer - label: 'Weight' + type: integer + label: 'Weight' 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 a4c0984c1a..0242569202 100644 --- a/core/modules/media_library/js/media_library.widget.es6.js +++ b/core/modules/media_library/js/media_library.widget.es6.js @@ -2,8 +2,55 @@ * @file media_library.widget.js */ (($, Drupal, window) => { - Drupal.media_library = { + /** + * Store the media library selection. + * + * When a user interacts with the media library we want the selection to + * persist as long as the media library modal is opened. We temporarily store + * the selected items while the user filters the media library view or + * navigates to different views pages and tabs. + */ + Drupal.mediaLibrarySelection = { selection: [], + /** + * Add a media item ID to the selection. + * + * @param {number} id + * The ID of the item we want to add. + */ + add(id) { + const position = this.selection.indexOf(id); + if (position === -1) { + this.selection.push(id); + } + }, + /** + * Remove a media item ID from the selection. + * + * @param {number} id + * The ID of the item we want to remove. + */ + remove(id) { + const position = this.selection.indexOf(id); + if (position !== -1) { + this.selection.splice(position, 1); + } + }, + /** + * Get the selected media items IDs. + * + * @return {Array} + * An array of selected media item IDs. + */ + get() { + return this.selection; + }, + /** + * Reset the selected media items IDs. + */ + reset() { + this.selection = []; + }, }; /** @@ -84,6 +131,36 @@ }, }; + /** + * Update the select button and number of selected items in the button pane. + */ + function updateButtonPane() { + const $buttonPane = $('.media-library-widget-modal .ui-dialog-buttonpane'); + if (!$buttonPane.length) { + return; + } + + const count = Drupal.mediaLibrarySelection.get().length; + const $toggleElements = $buttonPane.find( + '.media-library-select, .media-library-selected-count', + ); + + if (count === 0) { + // Hide the select button and selection count when nothing is + // selected. + $toggleElements.hide(); + } else { + // Add the selection count and show the select button. + const $wrapper = $buttonPane.find('.media-library-selected-count'); + if ($wrapper.length) { + $wrapper.replaceWith(Drupal.theme('mediaLibrarySelectionCount', count)); + } else { + $buttonPane.append(Drupal.theme('mediaLibrarySelectionCount', count)); + } + $toggleElements.fadeIn('fast'); + } + } + /** * Change the selection when a media item is checked/unchecked. */ @@ -93,82 +170,54 @@ '.js-media-library-item input[type="checkbox"]', context, ); - if ($mediaItems.length) { - $mediaItems.once('media-item-change').on('change', e => { - const $form = $(e.currentTarget).parents('.media-library-views-form'); - const position = Drupal.media_library.selection.indexOf(e.currentTarget.value); - const checked = $(e.currentTarget).is(':checked'); - if (checked && position === -1) { - Drupal.media_library.selection.push(e.currentTarget.value); - } else if (!checked && position !== -1) { - Drupal.media_library.selection.splice(position, 1); - } - // Set the selection in the hidden form element. - $form - .find('input#media-library-modal-selection') - .val(Drupal.media_library.selection.join()); + if (!$mediaItems.length) { + return; + } - // Prevent users from selecting more items than allowed in the view. - if ( - settings.media_library && - settings.media_library.selection_remaining - ) { - if ( - Drupal.media_library.selection.length === - settings.media_library.selection_remaining - ) { - $mediaItems - .not(':checked') - .prop('disabled', true) - .closest('.js-media-library-item') - .addClass('media-library-item--disabled'); - $mediaItems - .filter(':checked') - .prop('disabled', false) - .closest('.js-media-library-item') - .removeClass('media-library-item--disabled'); - } else { - $mediaItems - .prop('disabled', false) - .closest('.js-media-library-item') - .removeClass('media-library-item--disabled'); - } - } + function disableItems($items) { + $items + .prop('disabled', true) + .closest('.js-media-library-item') + .addClass('media-library-item--disabled'); + } - // Hide selection button if nothing is selected. We can't use the - // context here because the dialog copies the select button. - const $buttonPane = $( - '.media-library-widget-modal .ui-dialog-buttonpane', - ); - if (Drupal.media_library.selection.length === 0) { - $buttonPane - .find('.media-library-select, .media-library-selected-count') - .fadeOut('fast'); - } - else { - const selectItemsText = Drupal.formatPlural( - Drupal.media_library.selection.length, - '1 item selected', - '@count items selected', - ); - const $selectedItems = $buttonPane.find( - '.media-library-selected-count', - ); - if ($selectedItems.length) { - $selectedItems.html(selectItemsText); - } - else { - $buttonPane.append( - `
${selectItemsText}
`, - ); - } - $buttonPane - .find('.media-library-select, .media-library-selected-count') - .fadeIn('fast'); - } - }); + function enableItems($items) { + $items + .prop('disabled', false) + .closest('.js-media-library-item') + .removeClass('media-library-item--disabled'); } + + $mediaItems.once('media-item-change').on('change', e => { + const $form = $(e.currentTarget).parents('.media-library-views-form'); + + // Update the selection. + if ($(e.currentTarget).is(':checked')) { + Drupal.mediaLibrarySelection.add(e.currentTarget.value); + } else { + Drupal.mediaLibrarySelection.remove(e.currentTarget.value); + } + + // Set the selection in the hidden form element. + $form + .find('input#media-library-modal-selection') + .val(Drupal.mediaLibrarySelection.get().join()); + + // Once the selection is update, update the button pane. + updateButtonPane(); + + // Prevent users from selecting more items than allowed in the view. + if ( + Drupal.mediaLibrarySelection.get().length === + settings.media_library.selection_remaining + ) { + disableItems($mediaItems.not(':checked')); + enableItems($mediaItems.filter(':checked')); + } else { + enableItems($mediaItems); + } + }); }, }; @@ -176,77 +225,28 @@ * Apply the current selection when loading the media library view. */ Drupal.behaviors.MediaLibraryModalApplySelection = { - attach(context, settings) { + attach(context) { const $form = $('.media-library-views-form', context); - if ($form.length) { - if ($form.find('.js-media-library-item').length) { - // Select the items in the view. - Drupal.media_library.selection.forEach(value => { - $form - .find( - `.js-media-library-item input[type="checkbox"][value="${value}"]`, - ) - .prop('checked', true) - .trigger('change'); - }); - // Set the selection in the hidden form element. - $form - .find('input#media-library-modal-selection') - .val(Drupal.media_library.selection.join()); - - // Prevent users from selecting more items than allowed in the view. - if ( - settings.media_library && - settings.media_library.selection_remaining && - Drupal.media_library.selection.length === - settings.media_library.selection_remaining - ) { - $form - .find( - '.js-media-library-item input[type="checkbox"]', - ) - .not(':checked') - .prop('disabled', true) - .closest('.js-media-library-item') - .addClass('media-library-item--disabled'); - } - } + if (!$form.length) { + return; + } - // Hide selection button if nothing is selected. We can't use the - // context here because the dialog copies the select button. - $(window) - .once('media-library-toggle-buttons') - .on({ - 'dialog:aftercreate': () => { - const $buttonPane = $( - '.media-library-widget-modal .ui-dialog-buttonpane', - ); - if (Drupal.media_library.selection.length === 0) { - $buttonPane - .find('.media-library-select, .media-library-selected-count') - .hide(); - } else { - const selectItemsText = Drupal.formatPlural( - Drupal.media_library.selection.length, - '1 item selected', - '@count items selected', - ); - const $selectedItems = $buttonPane.find( - '.media-library-selected-count', - ); - if ($selectedItems.length) { - $selectedItems.html(selectItemsText); - } - else { - $buttonPane.append( - `
${selectItemsText}
`, - ); - } - } - }, - }); + if ($form.find('.js-media-library-item').length) { + // Select the items in the view. + Drupal.mediaLibrarySelection.get().forEach(value => { + $form + .find(`input[type="checkbox"][value="${value}"]`) + .prop('checked', true) + .trigger('change'); + }); } + + // Hide selection button if nothing is selected. We can't use the + // context here because the dialog copies the select button. + $(window) + .once('media-library-toggle-buttons') + .on('dialog:aftercreate', updateButtonPane); }, }; @@ -257,11 +257,27 @@ attach() { $(window) .once('media-library-clear-selection') - .on({ - 'dialog:afterclose': () => { - Drupal.media_library.selection = []; - }, + .on('dialog:afterclose', () => { + Drupal.mediaLibrarySelection.reset(); }); }, }; + + /** + * Theme function for the selection count. + * + * @param {number} count + * The number of selected items. + * + * @return {string} + * The corresponding HTML. + */ + Drupal.theme.mediaLibrarySelectionCount = function(count) { + const selectItemsText = Drupal.formatPlural( + count, + '1 item selected', + '@count items selected', + ); + return `
${selectItemsText}
`; + }; })(jQuery, Drupal, window); diff --git a/core/modules/media_library/js/media_library.widget.js b/core/modules/media_library/js/media_library.widget.js index 3707c3a598..d85ff3cae4 100644 --- a/core/modules/media_library/js/media_library.widget.js +++ b/core/modules/media_library/js/media_library.widget.js @@ -6,8 +6,26 @@ **/ (function ($, Drupal, window) { - Drupal.media_library = { - selection: [] + Drupal.mediaLibrarySelection = { + selection: [], + add: function add(id) { + var position = this.selection.indexOf(id); + if (position === -1) { + this.selection.push(id); + } + }, + remove: function remove(id) { + var position = this.selection.indexOf(id); + if (position !== -1) { + this.selection.splice(position, 1); + } + }, + get: function get() { + return this.selection; + }, + reset: function reset() { + this.selection = []; + } }; Drupal.behaviors.MediaLibraryWidgetSortable = { @@ -53,92 +71,95 @@ } }; + function updateButtonPane() { + var $buttonPane = $('.media-library-widget-modal .ui-dialog-buttonpane'); + if (!$buttonPane.length) { + return; + } + + var count = Drupal.mediaLibrarySelection.get().length; + var $toggleElements = $buttonPane.find('.media-library-select, .media-library-selected-count'); + + if (count === 0) { + $toggleElements.hide(); + } else { + var $wrapper = $buttonPane.find('.media-library-selected-count'); + if ($wrapper.length) { + $wrapper.replaceWith(Drupal.theme('mediaLibrarySelectionCount', count)); + } else { + $buttonPane.append(Drupal.theme('mediaLibrarySelectionCount', count)); + } + $toggleElements.fadeIn('fast'); + } + } + Drupal.behaviors.MediaLibraryModalChangeSelection = { attach: function attach(context, settings) { var $mediaItems = $('.js-media-library-item input[type="checkbox"]', context); - if ($mediaItems.length) { - $mediaItems.once('media-item-change').on('change', function (e) { - var $form = $(e.currentTarget).parents('.media-library-views-form'); - var position = Drupal.media_library.selection.indexOf(e.currentTarget.value); - var checked = $(e.currentTarget).is(':checked'); - if (checked && position === -1) { - Drupal.media_library.selection.push(e.currentTarget.value); - } else if (!checked && position !== -1) { - Drupal.media_library.selection.splice(position, 1); - } - - $form.find('input#media-library-modal-selection').val(Drupal.media_library.selection.join()); - - if (settings.media_library && settings.media_library.selection_remaining) { - if (Drupal.media_library.selection.length === settings.media_library.selection_remaining) { - $mediaItems.not(':checked').prop('disabled', true).closest('.js-media-library-item').addClass('media-library-item--disabled'); - $mediaItems.filter(':checked').prop('disabled', false).closest('.js-media-library-item').removeClass('media-library-item--disabled'); - } else { - $mediaItems.prop('disabled', false).closest('.js-media-library-item').removeClass('media-library-item--disabled'); - } - } - - var $buttonPane = $('.media-library-widget-modal .ui-dialog-buttonpane'); - if (Drupal.media_library.selection.length === 0) { - $buttonPane.find('.media-library-select, .media-library-selected-count').fadeOut('fast'); - } else { - var selectItemsText = Drupal.formatPlural(Drupal.media_library.selection.length, '1 item selected', '@count items selected'); - var $selectedItems = $buttonPane.find('.media-library-selected-count'); - if ($selectedItems.length) { - $selectedItems.html(selectItemsText); - } else { - $buttonPane.append('
' + selectItemsText + '
'); - } - $buttonPane.find('.media-library-select, .media-library-selected-count').fadeIn('fast'); - } - }); + + if (!$mediaItems.length) { + return; + } + + function disableItems($items) { + $items.prop('disabled', true).closest('.js-media-library-item').addClass('media-library-item--disabled'); } + + function enableItems($items) { + $items.prop('disabled', false).closest('.js-media-library-item').removeClass('media-library-item--disabled'); + } + + $mediaItems.once('media-item-change').on('change', function (e) { + var $form = $(e.currentTarget).parents('.media-library-views-form'); + + if ($(e.currentTarget).is(':checked')) { + Drupal.mediaLibrarySelection.add(e.currentTarget.value); + } else { + Drupal.mediaLibrarySelection.remove(e.currentTarget.value); + } + + $form.find('input#media-library-modal-selection').val(Drupal.mediaLibrarySelection.get().join()); + + updateButtonPane(); + + if (Drupal.mediaLibrarySelection.get().length === settings.media_library.selection_remaining) { + disableItems($mediaItems.not(':checked')); + enableItems($mediaItems.filter(':checked')); + } else { + enableItems($mediaItems); + } + }); } }; Drupal.behaviors.MediaLibraryModalApplySelection = { - attach: function attach(context, settings) { + attach: function attach(context) { var $form = $('.media-library-views-form', context); - if ($form.length) { - if ($form.find('.js-media-library-item').length) { - Drupal.media_library.selection.forEach(function (value) { - $form.find('.js-media-library-item input[type="checkbox"][value="' + value + '"]').prop('checked', true).trigger('change'); - }); - - $form.find('input#media-library-modal-selection').val(Drupal.media_library.selection.join()); - if (settings.media_library && settings.media_library.selection_remaining && Drupal.media_library.selection.length === settings.media_library.selection_remaining) { - $form.find('.js-media-library-item input[type="checkbox"]').not(':checked').prop('disabled', true).closest('.js-media-library-item').addClass('media-library-item--disabled'); - } - } + if (!$form.length) { + return; + } - $(window).once('media-library-toggle-buttons').on({ - 'dialog:aftercreate': function dialogAftercreate() { - var $buttonPane = $('.media-library-widget-modal .ui-dialog-buttonpane'); - if (Drupal.media_library.selection.length === 0) { - $buttonPane.find('.media-library-select, .media-library-selected-count').hide(); - } else { - var selectItemsText = Drupal.formatPlural(Drupal.media_library.selection.length, '1 item selected', '@count items selected'); - var $selectedItems = $buttonPane.find('.media-library-selected-count'); - if ($selectedItems.length) { - $selectedItems.html(selectItemsText); - } else { - $buttonPane.append('
' + selectItemsText + '
'); - } - } - } + if ($form.find('.js-media-library-item').length) { + Drupal.mediaLibrarySelection.get().forEach(function (value) { + $form.find('input[type="checkbox"][value="' + value + '"]').prop('checked', true).trigger('change'); }); } + + $(window).once('media-library-toggle-buttons').on('dialog:aftercreate', updateButtonPane); } }; Drupal.behaviors.MediaLibraryModalClearSelection = { attach: function attach() { - $(window).once('media-library-clear-selection').on({ - 'dialog:afterclose': function dialogAfterclose() { - Drupal.media_library.selection = []; - } + $(window).once('media-library-clear-selection').on('dialog:afterclose', function () { + Drupal.mediaLibrarySelection.reset(); }); } }; + + Drupal.theme.mediaLibrarySelectionCount = function (count) { + var selectItemsText = Drupal.formatPlural(count, '1 item selected', '@count items selected'); + return '
' + selectItemsText + '
'; + }; })(jQuery, Drupal, window); \ No newline at end of file diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module index 0a8df053d5..ae853827ed 100644 --- a/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -13,6 +13,7 @@ use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Template\Attribute; use Drupal\Core\Url; +use Drupal\media_library\MediaLibraryState; use Drupal\views\Form\ViewsForm; use Drupal\views\Plugin\views\cache\CachePluginBase; use Drupal\views\ViewExecutable; @@ -79,11 +80,8 @@ function media_library_views_post_render(ViewExecutable $view, &$output, CachePl if ($view->id() === 'media_library') { $output['#attached']['library'][] = 'media_library/view'; if ($view->current_display === 'widget') { - $query = array_intersect_key(\Drupal::request()->query->all(), array_flip([ - 'media_library_widget_id', - 'media_library_allowed_types', - 'media_library_remaining', - ])); + $state = MediaLibraryState::fromRequest(); + $query = $state->all(); // If the current query contains any parameters we use to contextually // filter the view, ensure they persist across AJAX rebuilds. // The ajax_path is shared for all AJAX views on the page, but our query diff --git a/core/modules/media_library/media_library.routing.yml b/core/modules/media_library/media_library.routing.yml index b53cea8d28..787b236078 100644 --- a/core/modules/media_library/media_library.routing.yml +++ b/core/modules/media_library/media_library.routing.yml @@ -9,4 +9,4 @@ media_library.modal: defaults: _controller: 'media_library.modal:build' requirements: - _permission: 'view media' + _custom_access: 'media_library.modal:access' diff --git a/core/modules/media_library/media_library.services.yml b/core/modules/media_library/media_library.services.yml index 645544938a..2c50cda166 100644 --- a/core/modules/media_library/media_library.services.yml +++ b/core/modules/media_library/media_library.services.yml @@ -1,4 +1,4 @@ services: media_library.modal: class: Drupal\media_library\MediaLibraryModal - arguments: ['@request_stack'] + arguments: ['@logger.factory'] diff --git a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php b/core/modules/media_library/src/Form/MediaLibraryUploadForm.php index 9cab9f7ee4..5f8c047f70 100644 --- a/core/modules/media_library/src/Form/MediaLibraryUploadForm.php +++ b/core/modules/media_library/src/Form/MediaLibraryUploadForm.php @@ -18,7 +18,7 @@ use Drupal\file\Plugin\Field\FieldType\FileItem; use Drupal\media\MediaInterface; use Drupal\media\MediaTypeInterface; -use Drupal\media_library\MediaLibraryConfiguration; +use Drupal\media_library\MediaLibraryState; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; @@ -315,24 +315,31 @@ public function selectType(array &$form, FormStateInterface $form_state) { * A command to send the selection to the current field widget. * * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException - * If the "media_library_widget_id" query parameter is not present. + * If the "media_library_state_id" query parameter is not present. */ public function updateWidget(array &$form, FormStateInterface $form_state) { if ($form_state->getErrors()) { return $form; } - $widget_id = $this->getRequest()->query->get('media_library_widget_id'); - if (!$widget_id || !is_string($widget_id)) { - throw new BadRequestHttpException('The "media_library_widget_id" query parameter is required and must be a string.'); + + $state = MediaLibraryState::fromRequest(); + $field_id = $state->getFieldId(); + $field_type = $state->getFieldType(); + if (!$field_id || !$field_type) { + throw new BadRequestHttpException('The media library state ID is required and must be a string.'); } + $mids = array_map(function (MediaInterface $media) { return $media->id(); }, $this->media); + // Pass the selection to the field widget based on the current widget ID. - return (new AjaxResponse()) - ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [implode(',', $mids)])) - ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown'])) - ->addCommand(new CloseDialogCommand()); + if ($field_type === 'entity_reference') { + return (new AjaxResponse()) + ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$field_id\"]", 'val', [implode(',', $mids)])) + ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$field_id\"]", 'trigger', ['mousedown'])) + ->addCommand(new CloseDialogCommand()); + } } /** @@ -481,12 +488,12 @@ public function access(array $allowed_types = NULL) { * A list of media types that are valid for this form. */ protected function getTypes(array $allowed_types = NULL) { - $media_library_configuration = MediaLibraryConfiguration::fromRequest(); + $state = MediaLibraryState::fromRequest(); // Cache results if possible. if (!isset($this->types)) { $media_type_storage = $this->entityTypeManager->getStorage('media_type'); if (!$allowed_types) { - $types = $media_library_configuration->getAllowedTypes(); + $types = $state->getAllowedTypes(); } else { $types = $media_type_storage->loadMultiple($allowed_types); diff --git a/core/modules/media_library/src/MediaLibraryConfiguration.php b/core/modules/media_library/src/MediaLibraryConfiguration.php deleted file mode 100644 index f9ddb5429e..0000000000 --- a/core/modules/media_library/src/MediaLibraryConfiguration.php +++ /dev/null @@ -1,99 +0,0 @@ -get('media_library_widget_id'); - } - - /** - * Returns the selected media type. - * - * @return \Drupal\media\Entity\MediaType - * The media type. - */ - public function getSelectedType() { - return MediaType::load($this->get('media_library_selected_type')); - } - - /** - * Returns the media types which can be selected. - * - * @return \Drupal\media\Entity\MediaType[] - * The media types. - */ - public function getAllowedTypes() { - $media_types = $this->get('media_library_allowed_types'); - return MediaType::loadMultiple($media_types); - } - - /** - * Determines if additional media items can be selected. - * - * @return bool - * TRUE if additional items can be selected, otherwise FALSE. - */ - public function hasSlotsAvailable() { - return $this->getAvailableSlots() !== 0; - } - - /** - * Returns the number of additional media items that can be selected. - * - * @return int - * The number of additional media items that can be selected. - */ - public function getAvailableSlots() { - return $this->getInt('media_library_remaining'); - } - - /** - * Get media library configuration from a request. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * (optional) The request. If not given, the current request will be used. - * - * @return \Drupal\media_library\MediaLibraryConfiguration - * A selection object. - */ - public static function fromRequest(Request $request = NULL) { - $request = $request ?: \Drupal::request(); - return new static($request->query->all()); - } - - /** - * Get media library dialog options. - * - * @return array - * The media library dialog options. - */ - public static function dialogOptions() { - return [ - 'dialogClass' => 'media-library-widget-modal', - 'title' => t('Media library'), - 'height' => '75%', - 'width' => '75%', - ]; - } - -} diff --git a/core/modules/media_library/src/MediaLibraryModal.php b/core/modules/media_library/src/MediaLibraryModal.php index 929419f0ca..8e15adbfd0 100644 --- a/core/modules/media_library/src/MediaLibraryModal.php +++ b/core/modules/media_library/src/MediaLibraryModal.php @@ -3,34 +3,48 @@ namespace Drupal\media_library; use Drupal\Component\Serialization\Json; -use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Access\AccessResult; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\media\MediaTypeInterface; use Drupal\views\Views; -use Symfony\Component\HttpFoundation\RequestStack; /** - * Controller which renders the media library modal. + * Service which renders the media library modal. */ class MediaLibraryModal { - use StringTranslationTrait; + /** + * The logger service. + * + * @var \Psr\Log\LoggerInterface + */ + protected $logger; /** - * The currently active request object. + * Constructs a MediaLibraryModal instance. * - * @var \Symfony\Component\HttpFoundation\Request + * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory + * The logger factory service. */ - protected $request; + public function __construct(LoggerChannelFactoryInterface $logger_factory) { + $this->logger = $logger_factory->get('media_library'); + } /** - * Constructs a MediaLibraryController instance. + * Get media library dialog options. * - * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack - * The currently active request object. + * @return array + * The media library dialog options. */ - public function __construct(RequestStack $request_stack) { - $this->request = $request_stack->getCurrentRequest(); + public static function dialogOptions() { + return [ + 'dialogClass' => 'media-library-widget-modal', + 'title' => t('Media library'), + 'height' => '75%', + 'width' => '75%', + ]; } /** @@ -40,7 +54,7 @@ public function __construct(RequestStack $request_stack) { * The render array for the media library. */ public function build() { - $configuration = MediaLibraryConfiguration::fromRequest(); + $state = MediaLibraryState::fromRequest(); return [ 'wrapper' => [ '#type' => 'html_tag', @@ -48,37 +62,61 @@ public function build() { '#attributes' => [ 'class' => ['media-library-wrapper'], ], - 'menu' => $this->getMediaTypeMenu($configuration), + 'menu' => $this->getMediaTypeMenu($state), 'content' => [ '#type' => 'html_tag', '#tag' => 'div', '#attributes' => [ 'class' => ['media-library-content'], ], - 'view' => $this->getMediaLibraryView($configuration->getSelectedType()), + 'view' => $this->getMediaLibraryView($state->getSelectedType()), ], ], ]; } + /** + * Check access to the media library. + * + * @param \Drupal\Core\Session\AccountInterface $account + * Run access checks for this account. + * + * @return \Drupal\Core\Access\AccessResult + * The access result. + */ + public function access(AccountInterface $account) { + // Deny access if the view or display are removed. + $view = Views::getView('media_library'); + if (!$view) { + $this->logger->error('The media library view does not exist.'); + return AccessResult::forbidden('The media library view does not exist.'); + } + if (!$view->storage->getDisplay('widget')) { + $this->logger->error('The media library view does not exist.'); + return AccessResult::forbidden('The media library widget display does not exist.'); + } + + return AccessResult::allowedIfHasPermission($account, 'view media'); + } + /** * Get the media type menu for the media library. * - * @param \Drupal\media_library\MediaLibraryConfiguration $configuration + * @param \Drupal\media_library\MediaLibraryState $state * The media library configuration. * * @return array * The render array for the media type menu. */ - protected function getMediaTypeMenu(MediaLibraryConfiguration $configuration) { - $selected_type = $configuration->getSelectedType(); - $allowed_types = $configuration->getAllowedTypes(); + protected function getMediaTypeMenu(MediaLibraryState $state) { + $selected_type = $state->getSelectedType(); + $allowed_types = $state->getAllowedTypes(); - $dialog_options = Json::encode(MediaLibraryConfiguration::dialogOptions()); + $dialog_options = Json::encode(static::dialogOptions()); // Add the menu for each type if we have more than 1 media type enabled for // the field. - if (count($allowed_types) <= 1) { + if (count($allowed_types) === 1) { return []; } @@ -87,18 +125,10 @@ protected function getMediaTypeMenu(MediaLibraryConfiguration $configuration) { '#links' => [], '#attributes' => [ 'class' => ['media-library-menu'], - ], - '#attached' =>[ - 'library' => ['core/drupal.vertical-tabs'], ] ]; - $query = [ - 'media_library_widget_id' => $configuration->getWidgetId(), - 'media_library_allowed_types' => array_keys($allowed_types), - 'media_library_remaining' => $configuration->getAvailableSlots(), - ]; - + $query = $state->all(); foreach ($allowed_types as $allowed_type_id => $allowed_type) { $query['media_library_selected_type'] = $allowed_type_id; $menu['#links'][$allowed_type_id] = [ @@ -133,13 +163,6 @@ protected function getMediaLibraryView(MediaTypeInterface $media_type) { $view = Views::getView('media_library'); $display_id = 'widget'; - // Add an extra check because the view could have been deleted. - if (!is_object($view)) { - // @todo: throw error when view doesn't exist? - return []; - } - - // @todo: throw error when display doesn't exist? $media_type_id = $media_type->id(); $view->setDisplay($display_id); $view->preExecute([$media_type_id]); diff --git a/core/modules/media_library/src/MediaLibraryState.php b/core/modules/media_library/src/MediaLibraryState.php new file mode 100644 index 0000000000..8b16098915 --- /dev/null +++ b/core/modules/media_library/src/MediaLibraryState.php @@ -0,0 +1,128 @@ +get('media_library_field_id'); + } + + /** + * Returns the field type of the field that opened the modal. + * + * @return string + * The field type. + */ + public function getFieldType() { + return $this->get('media_library_field_type'); + } + + /** + * Returns the selected media type. + * + * @return \Drupal\media\Entity\MediaType + * The media type. + */ + public function getSelectedType() { + return MediaType::load($this->get('media_library_selected_type')); + } + + /** + * Returns the media types which can be selected. + * + * @return \Drupal\media\Entity\MediaType[] + * The media types. + */ + public function getAllowedTypes() { + // When no media types are passed, we load all media types since that is the + // default behaviour of the entity reference field target bundles. + $media_types = $this->get('media_library_allowed_types') ?: NULL; + return MediaType::loadMultiple($media_types); + } + + /** + * Determines if additional media items can be selected. + * + * @return bool + * TRUE if additional items can be selected, otherwise FALSE. + */ + public function hasSlotsAvailable() { + return $this->getAvailableSlots() !== 0; + } + + /** + * Returns the number of additional media items that can be selected. + * + * @return int + * The number of additional media items that can be selected. + */ + public function getAvailableSlots() { + return $this->getInt('media_library_remaining'); + } + + /** + * Create a new media library URL with state parameters. + * + * @param string $field_id + * The field ID. + * @param string $field_type + * The field type. + * @param string $selected_type_id + * The selected media type ID. + * @param string[] $allowed_media_type_ids + * The allowed media type IDs. + * @param int $remaining + * The number of remaining items the user is allowed to select or add in the + * library. + * + * @return \Drupal\Core\Url + * A new Url object for the media library. + */ + public static function createUrl($field_id, $field_type, $selected_type_id, array $allowed_media_type_ids, $remaining) { + $query = [ + 'media_library_field_id' => $field_id, + 'media_library_field_type' => $field_type, + 'media_library_selected_type' => $selected_type_id, + 'media_library_allowed_types' => $allowed_media_type_ids, + 'media_library_remaining' => $remaining, + ]; + return Url::fromRoute('media_library.modal', [], [ + 'query' => $query, + ]); + } + + /** + * Get the media library state from a request. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * (optional) The request. If not given, the current request will be used. + * + * @return \Drupal\media_library\MediaLibraryState + * A selection object. + */ + public static function fromRequest(Request $request = NULL) { + $request = $request ?: \Drupal::request(); + return new static($request->query->all()); + } + +} 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 6c721486d2..f963290d1e 100644 --- a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php @@ -12,10 +12,12 @@ use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Render\Element; use Drupal\Core\Url; use Drupal\media\Entity\Media; use Drupal\media_library\Form\MediaLibraryUploadForm; -use Drupal\media_library\MediaLibraryConfiguration; +use Drupal\media_library\MediaLibraryModal; +use Drupal\media_library\MediaLibraryState; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Validator\ConstraintViolationInterface; @@ -117,15 +119,24 @@ public static function defaultSettings() { protected function getEnabledMediaTypeIdsSorted() { $media_types_setting = $this->getSetting('media_types'); $configured_media_type_ids = array_keys($this->getFieldSetting('handler_settings')['target_bundles']); + if (empty($media_types_setting)) { return $configured_media_type_ids; } - uasort($media_types_setting, 'Drupal\Component\Utility\SortArray::sortByWeightElement'); + + asort($media_types_setting); $sorted_media_type_ids = array_keys($media_types_setting); - // Add new media types. - $sorted_media_type_ids = array_merge($sorted_media_type_ids, array_diff($configured_media_type_ids, $sorted_media_type_ids)); - // Remove deletes types. - return array_intersect($sorted_media_type_ids, $configured_media_type_ids); + + // There could have been added or removed media types in the field storage. + // We need to make sure new media types are added to the list and remove + // media types that are no longer available for the field. + $new_media_type_ids = array_diff($configured_media_type_ids, $sorted_media_type_ids); + // Add new media type IDs to the list. + $sorted_media_type_ids = array_merge($sorted_media_type_ids, $new_media_type_ids); + // Remove media types that are no longer available. + $sorted_media_type_ids = array_intersect($sorted_media_type_ids, $configured_media_type_ids); + + return $sorted_media_type_ids; } /** @@ -133,11 +144,11 @@ protected function getEnabledMediaTypeIdsSorted() { */ public function settingsForm(array $form, FormStateInterface $form_state) { $media_type_ids = $this->getEnabledMediaTypeIdsSorted(); - if (count($media_type_ids) > 1) { + if (count($media_type_ids) !== 1) { $form['media_types'] = [ '#type' => 'table', '#header' => [ - $this->t('Sort media types'), + $this->t('Tab order'), $this->t('Weight'), ], '#tabledrag' => [ @@ -164,6 +175,7 @@ public function settingsForm(array $form, FormStateInterface $form_state) { ], '#weight' => $delta, '#attributes' => ['class' => ['draggable']], + '#process' => [[static::class, 'processMediaTypeParents']], ]; $delta++; } @@ -171,19 +183,50 @@ public function settingsForm(array $form, FormStateInterface $form_state) { return $form; } + /** + * Process callback to optimize the way the media type weights are stored. + * + * The tabledrag functionality needs a specific weight field, but we don't + * this extra weight field in our settings. To remove this, we need to change + * the #parents array of the weight field. We also need to change this from + * the wrapper element, since the form builder handles input before processing + * an element. + * + * @param array $element + * The media type weight element. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state. + * + * @return array + * The changed element. + * + * @see \Drupal\Core\Form\FormBuilder::doBuildForm() + */ + public static function processMediaTypeParents(array $element, FormStateInterface $form_state) { + foreach (Element::children($element) as $key) { + if (!isset($element[$key]['#tree'])) { + $element[$key]['#tree'] = $element['#tree']; + } + if (!isset($element[$key]['#parents'])) { + $element[$key]['#parents'] = $element[$key]['#tree'] && $element['#tree'] ? $element['#parents'] : [$key]; + } + } + return $element; + } + /** * {@inheritdoc} */ public function settingsSummary() { $summary = []; - $media_type_labels = []; $media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($this->getEnabledMediaTypeIdsSorted()); - foreach ($media_types as $media_type) { - $media_type_labels[] = $media_type->label(); + if ($media_types !== 1) { + foreach ($media_types as $media_type) { + $media_type_labels[] = $media_type->label(); + } + $summary[] = t('Tab order: @order', ['@order' => implode(', ', $media_type_labels)]); } - $summary[] = t('Media type order: @order', ['@order' => implode(', ', $media_type_labels)]); - return $summary; } @@ -321,15 +364,14 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen $element['#description'] .= '
' . $cardinality_message; } + // Create a new media library URL with the correct state parameters. $media_type_ids = $this->getEnabledMediaTypeIdsSorted(); $primary_media_type_id = array_slice($media_type_ids, 0, 1, TRUE); - $query = [ - 'media_library_widget_id' => $field_name . $id_suffix, - 'media_library_selected_type' => reset($primary_media_type_id), - 'media_library_allowed_types' => $media_type_ids, - 'media_library_remaining' => $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining, - ]; - $dialog_options = Json::encode(MediaLibraryConfiguration::dialogOptions()); + $selected_type = reset($primary_media_type_id); + $remaining = $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining; + $url = MediaLibraryState::createUrl($field_name . $id_suffix, 'entity_reference', $selected_type, $media_type_ids, $remaining); + + $dialog_options = Json::encode(MediaLibraryModal::dialogOptions()); // Add a button that will load the Media library in a modal using AJAX. $element['media_library_open_button'] = [ @@ -337,9 +379,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#title' => $this->t('Browse media'), '#name' => $field_name . '-media-library-open-button' . $id_suffix, // @todo Make the view configurable in https://www.drupal.org/project/drupal/issues/2971209 - '#url' => Url::fromRoute('media_library.modal', [], [ - 'query' => $query, - ]), + '#url' => $url, '#attributes' => [ 'class' => ['button', 'use-ajax', 'media-library-open-button'], 'data-dialog-type' => 'modal', diff --git a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php index d7e94b27c1..10379f5dcd 100644 --- a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php +++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php @@ -8,10 +8,11 @@ use Drupal\Core\Form\FormBuilderInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Url; -use Drupal\media_library\MediaLibraryConfiguration; +use Drupal\media_library\MediaLibraryState; use Drupal\views\Plugin\views\field\FieldPluginBase; use Drupal\views\Render\ViewsRenderPipelineMarkup; use Drupal\views\ResultRow; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** * Defines a field that outputs a checkbox and form for selecting media. @@ -107,19 +108,23 @@ public static function updateWidget(array &$form, FormStateInterface $form_state $field_id = $form_state->getTriggeringElement()['#field_id']; $selected = array_filter(explode(',', $form_state->getValue($field_id, []))); - $media_library_configuration = MediaLibraryConfiguration::fromRequest(); - $response = new AjaxResponse(); $response->addCommand(new CloseDialogCommand()); - $widget_id = $media_library_configuration->getWidgetId(); + $state = MediaLibraryState::fromRequest(); + $field_id = $state->getFieldId(); + $field_type = $state->getFieldType(); + if (!$field_id || !$field_type) { + throw new BadRequestHttpException('The media library field ID and field type are required and must be a string.'); + } $ids = implode(',', $selected); - if ($widget_id && is_string($widget_id)) { + if ($field_type === 'entity_reference') { $response - ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [$ids])) - ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown'])); + ->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$field_id\"]", 'val', [$ids])) + ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$field_id\"]", 'trigger', ['mousedown'])); } + return $response; } diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php index 9f4d1ad5d8..9866810781 100644 --- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php @@ -198,10 +198,10 @@ public function testWidget() { $assert_session->assertWaitOnAjaxRequest(); $page->find('css', '.tabledrag-toggle-weight')->click(); $edit = [ - 'fields[field_twin_media][settings_edit_form][settings][media_types][type_one][weight]' => 0, - 'fields[field_twin_media][settings_edit_form][settings][media_types][type_three][weight]' => 1, - 'fields[field_twin_media][settings_edit_form][settings][media_types][type_four][weight]' => 2, - 'fields[field_twin_media][settings_edit_form][settings][media_types][type_two][weight]' => 3, + 'fields[field_twin_media][settings_edit_form][settings][media_types][type_one]' => 0, + 'fields[field_twin_media][settings_edit_form][settings][media_types][type_three]' => 1, + 'fields[field_twin_media][settings_edit_form][settings][media_types][type_four]' => 2, + 'fields[field_twin_media][settings_edit_form][settings][media_types][type_two]' => 3, ]; $this->drupalPostForm(NULL, $edit, t('Save')); $page->find('css', '.tabledrag-toggle-weight')->click();