diff --git a/core/modules/media/media.module b/core/modules/media/media.module index 20d12b856cc5259d896d477094ea7ab8721f5c7e..a95b972d66565a9af43f446fb5de719c898db858 100644 --- a/core/modules/media/media.module +++ b/core/modules/media/media.module @@ -243,31 +243,9 @@ function media_field_widget_multivalue_form_alter(array &$elements, FormStateInt // Retrieve the media bundle list and add information for the user based on // which bundles are available to be created or referenced. $settings = $context['items']->getFieldDefinition()->getSetting('handler_settings'); - $allowed_bundles = isset($settings['target_bundles']) ? $settings['target_bundles'] : []; - $access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media'); - $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media'); - $bundle_labels = []; - $create_bundles = []; - foreach ($allowed_bundles as $bundle) { - $bundle_labels[] = $all_bundles[$bundle]['label']; - if ($access_handler->createAccess($bundle)) { - $create_bundles[] = $bundle; - if (count($create_bundles) > 1) { - // If the user has access to create more than 1 bundle then the - // individual media type form can not be used. - break; - } - } - } - - // Add a section about how to create media if the user has access to do so. - if (!empty($create_bundles)) { - if (count($create_bundles) === 1) { - $add_url = Url::fromRoute('entity.media.add_form', ['media_type' => $create_bundles[0]])->toString(); - } - elseif (count($create_bundles) > 1) { - $add_url = Url::fromRoute('entity.media.add_page')->toString(); - } + $allowed_bundles = !empty($settings['target_bundles']) ? $settings['target_bundles'] : []; + $add_url = _media_get_add_url($allowed_bundles); + if ($add_url) { $elements['#media_help']['#media_add_help'] = t('Create your media on the media add page (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]); } @@ -308,6 +286,10 @@ function media_field_widget_multivalue_form_alter(array &$elements, FormStateInt if ($overview_url->access()) { $elements['#media_help']['#media_list_link'] = t('See the media list (opens a new window) to help locate media.', [':list_url' => $overview_url->toString()]); } + $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media'); + $bundle_labels = array_map(function ($bundle) use ($all_bundles) { + return $all_bundles[$bundle]['label']; + }, $allowed_bundles); $elements['#media_help']['#allowed_types_help'] = t('Allowed media types: %types', ['%types' => implode(", ", $bundle_labels)]); } } @@ -336,3 +318,32 @@ function media_preprocess_media_reference_help(&$variables) { } } } + +/** + * Returns the appropriate URL to add media for the current user. + * + * @todo Remove in https://www.drupal.org/project/drupal/issues/2938116 + * + * @param string[] $allowed_bundles + * An array of bundles that should be checked for create access. + * + * @return bool|\Drupal\Core\Url + * The URL to add media, or FALSE if the user cannot create any media. + * + * @internal + * This function is internal and may be removed in a minor release. + */ +function _media_get_add_url($allowed_bundles) { + $access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media'); + $create_bundles = array_filter($allowed_bundles, [$access_handler, 'createAccess']); + + // Add a section about how to create media if the user has access to do so. + if (count($create_bundles) === 1) { + return Url::fromRoute('entity.media.add_form', ['media_type' => reset($create_bundles)])->toString(); + } + elseif (count($create_bundles) > 1) { + return Url::fromRoute('entity.media.add_page')->toString(); + } + + return FALSE; +} diff --git a/core/modules/media_library/config/install/views.view.media_library.yml b/core/modules/media_library/config/install/views.view.media_library.yml index 61ac13600c6efe88eb9366707922180f0ee3daaa..32096a6f9cc0203bb40cf1ceb3d276db24354183 100644 --- a/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 @@ dependencies: - media_library module: - media + - media_library - user id: media_library label: 'Media library' @@ -71,7 +72,7 @@ display: type: default options: grouping: { } - row_class: 'media-library-item js-click-to-select' + row_class: 'media-library-item js-media-library-item js-click-to-select' default_row_class: true row: type: fields @@ -118,7 +119,7 @@ display: preserve_tags: '' html: false element_type: '' - element_class: js-click-to-select__checkbox + element_class: js-click-to-select-checkbox element_label_type: '' element_label_class: '' element_label_colon: false @@ -376,51 +377,9 @@ display: content: 'No media available.' plugin_id: text_custom relationships: { } - arguments: - bundle: - id: bundle - table: media_field_data - field: bundle - relationship: none - group_type: group - admin_label: '' - default_action: ignore - exception: - value: all - title_enable: false - title: All - title_enable: false - title: '' - default_argument_type: fixed - default_argument_options: - argument: '' - default_argument_skip_url: false - summary_options: - base_path: '' - count: true - items_per_page: 25 - override: false - summary: - sort_order: asc - number_of_records: 0 - format: default_summary - specify_validation: false - validate: - type: none - fail: 'not found' - validate_options: { } - glossary: false - limit: 0 - case: none - path_case: none - transform_dash: false - break_phrase: false - entity_type: media - entity_field: bundle - plugin_id: string display_extenders: { } use_ajax: true - css_class: media-library-view + css_class: 'media-library-view js-media-library-view' cache_metadata: max-age: 0 contexts: @@ -456,3 +415,131 @@ display: - 'url.query_args:sort_by' - user.permissions tags: { } + # @todo Lock down access in https://www.drupal.org/node/2983179 + 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: js-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 + access: false + display_description: '' + access: + type: perm + options: + perm: 'view media' + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_interface' + - url + - url.query_args + - 'url.query_args:sort_by' + - user.permissions + tags: { } diff --git a/core/modules/media_library/css/media_library.module.css b/core/modules/media_library/css/media_library.module.css index 11ad56dd922e3fd31b30b8f5521b1df5bd47289d..89b15800634a733bd24b59721257500846f1d18f 100644 --- a/core/modules/media_library/css/media_library.module.css +++ b/core/modules/media_library/css/media_library.module.css @@ -2,22 +2,19 @@ * @file media_library.module.css */ -.media-library-page-form { - display: flex; - flex-wrap: wrap; -} - -.media-library-page-form > .form-actions { +.media-library-views-form > .form-actions { flex-basis: 100%; } -.media-library-page-form__header > div, +.media-library-views-form, +.media-library-selection, +.media-library-views-form__bulk_form, .media-library-view .form--inline { display: flex; flex-wrap: wrap; } -.media-library-page-form__header { +.media-library-views-form__header { flex-basis: 100%; } @@ -25,7 +22,7 @@ position: relative; } -.media-library-item .js-click-to-select__trigger { +.media-library-item .js-click-to-select-trigger { overflow: hidden; cursor: pointer; } @@ -34,7 +31,7 @@ align-self: flex-end; } -.media-library-item .js-click-to-select__checkbox { +.media-library-item .js-click-to-select-checkbox { position: absolute; display: block; z-index: 1; @@ -51,6 +48,25 @@ .media-library-select-all { flex-basis: 100%; + width: 100%; +} + +.media-library-view.view-display-id-widget .media-library-select-all { + display: none; +} + +.media-library-item--disabled { + pointer-events: none; +} + +.media-library-selection .media-library-item__preview { + cursor: move; +} + +/* @todo Remove or re-work in https://www.drupal.org/node/2985168 */ +.media-library-widget .media-library-item__name a, +.media-library-view.view-display-id-widget .media-library-item__name a { + pointer-events: none; } @media screen and (max-width: 600px) { diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css index 0427143538e36cca88518bc430209ad86d519ee7..5aab3c5bbd17118340a3d677868267aabfb94505 100644 --- a/core/modules/media_library/css/media_library.theme.css +++ b/core/modules/media_library/css/media_library.theme.css @@ -5,7 +5,7 @@ * @see https://www.drupal.org/project/drupal/issues/2980769 */ -.media-library-page-form__header .form-item { +.media-library-views-form__header .form-item { margin-right: 8px; } @@ -16,21 +16,29 @@ .media-library-item { justify-content: center; vertical-align: top; - padding: 2px; - border: 1px solid #ebebeb; + border: 1px solid #dbdbdb; margin: 16px 16px 2px 2px; width: 180px; background: #fff; transition: border-color 0.2s, color 0.2s, background 0.2s; } +.media-library-view { + min-height: 300px; +} + .media-library-view .form-actions { margin: 0.75em 0; } +.media-library-view .media-library-view--form-actions { + clear: left; + margin: 0.75em 0; + align-self: flex-end; +} + .media-library-item .field--name-thumbnail { background-color: #ebebeb; - margin: 2px; overflow: hidden; text-align: center; } @@ -54,17 +62,17 @@ border-color: #0076c0; } -.media-library-item .js-click-to-select__checkbox input { +.media-library-item .js-click-to-select-checkbox input { width: 30px; height: 30px; } -.media-library-item .js-click-to-select__checkbox .form-item { +.media-library-item .js-click-to-select-checkbox .form-item { margin: 0; } .media-library-item__preview { - padding-bottom: 44px; + padding-bottom: 34px; } .media-library-item__status { @@ -90,9 +98,9 @@ position: absolute; bottom: 0; display: block; - padding: 10px; - max-width: calc(100% - 20px); - max-height: calc(100% - 60px); + padding: 5px; + max-width: calc(100% - 10px); + max-height: calc(100% - 50px); overflow: hidden; background: white; } @@ -135,6 +143,68 @@ margin-right: 10px; } +.media-library-item--disabled { + opacity: 0.5; +} + +.media-library-selection { + margin-bottom: 1.5rem; +} + +.media-library-widget { + position: relative; +} + +.media-library-widget__toggle-weight { + position: absolute; + right: 5px; + top: 5px; +} + +.media-library-item .form-item { + margin: 0.75em; +} + +.media-library-item__remove, +.media-library-item__remove:hover, +.media-library-item__remove:focus, +.media-library-item__remove.button, +.media-library-item__remove.button:disabled, +.media-library-item__remove.button:disabled:active, +.media-library-item__remove.button:hover, +.media-library-item__remove.button:focus { + position: absolute; + z-index: 1; + top: 0; + right: 0; + width: 24px; + height: 24px; + margin: 5px; + padding: 0; + background: url('../../../misc/icons/787878/ex.svg') #fff center no-repeat; + background-size: 16px 16px; + border: 2px solid #ccc; + border-radius: 20px; + color: transparent; + text-shadow: none; + transition: 0.2s border-color; +} + +.media-library-item__remove:hover, +.media-library-item__remove:focus, +.media-library-item__remove.button:hover, +.media-library-item__remove.button:focus, +.media-library-item__remove.button:disabled:active { + border-color: #40b6ff; +} + +/* @todo Remove or re-work in https://www.drupal.org/node/2985168 */ +.media-library-widget .media-library-item__name a, +.media-library-view.view-display-id-widget .media-library-item__name a { + color: black; + text-decoration: none; +} + @media screen and (max-width: 600px) { .media-library-item { width: 150px; diff --git a/core/modules/media_library/js/media_library.click_to_select.es6.js b/core/modules/media_library/js/media_library.click_to_select.es6.js index 321ffe0c73a9f9f056123d10fad095d832d2a1e4..d5179c34997c43084ac958eaf4fd276df378d56d 100644 --- a/core/modules/media_library/js/media_library.click_to_select.es6.js +++ b/core/modules/media_library/js/media_library.click_to_select.es6.js @@ -8,7 +8,7 @@ */ Drupal.behaviors.ClickToSelect = { attach(context) { - $('.js-click-to-select__trigger', context) + $('.js-click-to-select-trigger', context) .once('media-library-click-to-select') .on('click', (event) => { // Links inside the trigger should not be click-able. @@ -16,10 +16,10 @@ // Click the hidden checkbox when the trigger is clicked. const $input = $(event.currentTarget) .closest('.js-click-to-select') - .find('.js-click-to-select__checkbox input'); + .find('.js-click-to-select-checkbox input'); $input.prop('checked', !$input.prop('checked')).trigger('change'); }); - $('.js-click-to-select__checkbox input', context) + $('.js-click-to-select-checkbox input', context) .once('media-library-click-to-select') .on('change', ({ currentTarget }) => { $(currentTarget) diff --git a/core/modules/media_library/js/media_library.click_to_select.js b/core/modules/media_library/js/media_library.click_to_select.js index 4bc041c43e56a72a8047b1bc413d4e3d445e4cbf..01cbb1431b4c15081848868ce6e14c60b938a1cd 100644 --- a/core/modules/media_library/js/media_library.click_to_select.js +++ b/core/modules/media_library/js/media_library.click_to_select.js @@ -8,13 +8,13 @@ (function ($, Drupal) { Drupal.behaviors.ClickToSelect = { attach: function attach(context) { - $('.js-click-to-select__trigger', context).once('media-library-click-to-select').on('click', function (event) { + $('.js-click-to-select-trigger', context).once('media-library-click-to-select').on('click', function (event) { event.preventDefault(); - var $input = $(event.currentTarget).closest('.js-click-to-select').find('.js-click-to-select__checkbox input'); + var $input = $(event.currentTarget).closest('.js-click-to-select').find('.js-click-to-select-checkbox input'); $input.prop('checked', !$input.prop('checked')).trigger('change'); }); - $('.js-click-to-select__checkbox input', context).once('media-library-click-to-select').on('change', function (_ref) { + $('.js-click-to-select-checkbox input', context).once('media-library-click-to-select').on('change', function (_ref) { var currentTarget = _ref.currentTarget; $(currentTarget).closest('.js-click-to-select').toggleClass('checked', $(currentTarget).prop('checked')); diff --git a/core/modules/media_library/js/media_library.view.es6.js b/core/modules/media_library/js/media_library.view.es6.js index 26e9a5b2635ef05e7276c636a063207d9383f290..98b85c9ed1592e4818a83cb9415de937fa260880 100644 --- a/core/modules/media_library/js/media_library.view.es6.js +++ b/core/modules/media_library/js/media_library.view.es6.js @@ -7,7 +7,7 @@ */ Drupal.behaviors.MediaLibraryHover = { attach(context) { - $('.media-library-item .js-click-to-select__trigger,.media-library-item .js-click-to-select__checkbox', context).once('media-library-item-hover') + $('.media-library-item .js-click-to-select-trigger,.media-library-item .js-click-to-select-checkbox', context).once('media-library-item-hover') .on('mouseover mouseout', ({ currentTarget, type }) => { $(currentTarget).closest('.media-library-item').toggleClass('is-hover', type === 'mouseover'); }); @@ -19,7 +19,7 @@ */ Drupal.behaviors.MediaLibraryFocus = { attach(context) { - $('.media-library-item .js-click-to-select__checkbox input', context).once('media-library-item-focus') + $('.media-library-item .js-click-to-select-checkbox input', context).once('media-library-item-focus') .on('focus blur', ({ currentTarget, type }) => { $(currentTarget).closest('.media-library-item').toggleClass('is-focus', type === 'focus'); }); diff --git a/core/modules/media_library/js/media_library.view.js b/core/modules/media_library/js/media_library.view.js index 1cde60c3acfb633d4c792b2d70cba731a19d0e02..90573005711e30f24819b47a0bb8ed68e4158497 100644 --- a/core/modules/media_library/js/media_library.view.js +++ b/core/modules/media_library/js/media_library.view.js @@ -8,7 +8,7 @@ (function ($, Drupal) { Drupal.behaviors.MediaLibraryHover = { attach: function attach(context) { - $('.media-library-item .js-click-to-select__trigger,.media-library-item .js-click-to-select__checkbox', context).once('media-library-item-hover').on('mouseover mouseout', function (_ref) { + $('.media-library-item .js-click-to-select-trigger,.media-library-item .js-click-to-select-checkbox', context).once('media-library-item-hover').on('mouseover mouseout', function (_ref) { var currentTarget = _ref.currentTarget, type = _ref.type; @@ -19,7 +19,7 @@ Drupal.behaviors.MediaLibraryFocus = { attach: function attach(context) { - $('.media-library-item .js-click-to-select__checkbox input', context).once('media-library-item-focus').on('focus blur', function (_ref2) { + $('.media-library-item .js-click-to-select-checkbox input', context).once('media-library-item-focus').on('focus blur', function (_ref2) { var currentTarget = _ref2.currentTarget, type = _ref2.type; diff --git a/core/modules/media_library/js/media_library.widget.es6.js b/core/modules/media_library/js/media_library.widget.es6.js new file mode 100644 index 0000000000000000000000000000000000000000..654c2cde2871244fef91b9573f627364e35f3ee6 --- /dev/null +++ b/core/modules/media_library/js/media_library.widget.es6.js @@ -0,0 +1,93 @@ +/** + * @file media_library.widget.js + */ +(($, Drupal) => { + /** + * Allows users to re-order their selection with drag+drop. + */ + Drupal.behaviors.MediaLibraryWidgetSortable = { + attach(context) { + // Allow media items to be re-sorted with drag+drop in the widget. + $('.js-media-library-selection', context).once('media-library-sortable').sortable({ + tolerance: 'pointer', + helper: 'clone', + handle: '.js-media-library-item-preview', + stop: ({ target }) => { + // Update all the hidden "weight" fields. + $(target).children().each((index, child) => { + $(child).find('.js-media-library-item-weight').val(index); + }); + }, + }); + }, + }; + + /** + * Allows selection order to be set without drag+drop for accessibility. + */ + Drupal.behaviors.MediaLibraryWidgetToggleWeight = { + attach(context) { + const strings = { + show: Drupal.t('Show media item weights'), + hide: Drupal.t('Hide media item weights'), + }; + $('.js-media-library-widget-toggle-weight', context).once('media-library-toggle') + .on('click', (e) => { + e.preventDefault(); + $(e.currentTarget) + .toggleClass('active') + .text($(e.currentTarget).hasClass('active') ? strings.hide : strings.show) + .parent() + .find('.js-media-library-item-weight') + .parent() + .toggle(); + }) + .text(strings.show); + $('.js-media-library-item-weight', context).once('media-library-toggle').parent().hide(); + }, + }; + + /** + * Warn users when clicking outgoing links from the library or widget. + */ + Drupal.behaviors.MediaLibraryWidgetWarn = { + attach(context) { + $('.js-media-library-item a[href]', context).once('media-library-warn-link') + .on('click', (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(); + } + }); + }, + }; + + /** + * Prevent users from selecting more items than allowed in the view. + */ + Drupal.behaviors.MediaLibraryWidgetRemaining = { + attach(context, settings) { + const $view = $('.js-media-library-view', context).once('media-library-remaining'); + $view.find('.js-media-library-item input[type="checkbox"]') + .on('change', () => { + if (settings.media_library && settings.media_library.selection_remaining) { + const $checkboxes = $view.find('.js-media-library-item input[type="checkbox"]'); + if ($checkboxes.filter(':checked').length === settings.media_library.selection_remaining) { + $checkboxes + .not(':checked') + .prop('disabled', true) + .closest('.js-media-library-item') + .addClass('media-library-item--disabled'); + } + else { + $checkboxes + .prop('disabled', false) + .closest('.js-media-library-item') + .removeClass('media-library-item--disabled'); + } + } + }); + }, + }; +})(jQuery, Drupal); diff --git a/core/modules/media_library/js/media_library.widget.js b/core/modules/media_library/js/media_library.widget.js new file mode 100644 index 0000000000000000000000000000000000000000..ad6fbd76e5de4491671a19952bf1039da2566e79 --- /dev/null +++ b/core/modules/media_library/js/media_library.widget.js @@ -0,0 +1,67 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(function ($, Drupal) { + Drupal.behaviors.MediaLibraryWidgetSortable = { + attach: function attach(context) { + $('.js-media-library-selection', context).once('media-library-sortable').sortable({ + tolerance: 'pointer', + helper: 'clone', + handle: '.js-media-library-item-preview', + stop: function stop(_ref) { + var target = _ref.target; + + $(target).children().each(function (index, child) { + $(child).find('.js-media-library-item-weight').val(index); + }); + } + }); + } + }; + + Drupal.behaviors.MediaLibraryWidgetToggleWeight = { + attach: function attach(context) { + var strings = { + show: Drupal.t('Show media item weights'), + hide: Drupal.t('Hide media item weights') + }; + $('.js-media-library-widget-toggle-weight', context).once('media-library-toggle').on('click', function (e) { + e.preventDefault(); + $(e.currentTarget).toggleClass('active').text($(e.currentTarget).hasClass('active') ? strings.hide : strings.show).parent().find('.js-media-library-item-weight').parent().toggle(); + }).text(strings.show); + $('.js-media-library-item-weight', context).once('media-library-toggle').parent().hide(); + } + }; + + Drupal.behaviors.MediaLibraryWidgetWarn = { + attach: function attach(context) { + $('.js-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.behaviors.MediaLibraryWidgetRemaining = { + attach: function attach(context, settings) { + var $view = $('.js-media-library-view', context).once('media-library-remaining'); + $view.find('.js-media-library-item input[type="checkbox"]').on('change', function () { + if (settings.media_library && settings.media_library.selection_remaining) { + var $checkboxes = $view.find('.js-media-library-item input[type="checkbox"]'); + if ($checkboxes.filter(':checked').length === settings.media_library.selection_remaining) { + $checkboxes.not(':checked').prop('disabled', true).closest('.js-media-library-item').addClass('media-library-item--disabled'); + } else { + $checkboxes.prop('disabled', false).closest('.js-media-library-item').removeClass('media-library-item--disabled'); + } + } + }); + } + }; +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/modules/media_library/media_library.libraries.yml b/core/modules/media_library/media_library.libraries.yml index e32dbe074b496aa5b19cb47fede291b5d86ce54b..848222543e4f300c24ad758f601b2883105e2a28 100644 --- a/core/modules/media_library/media_library.libraries.yml +++ b/core/modules/media_library/media_library.libraries.yml @@ -21,5 +21,14 @@ view: dependencies: - media_library/style - media_library/click_to_select + +widget: + version: VERSION + js: + js/media_library.widget.js: {} + dependencies: + - core/drupal.ajax + - core/jquery.ui.sortable + - media_library/view - core/drupal.announce - core/jquery.once diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module index 63b3cadb96a8e4f56030b9829f6a47f43be969ba..b72a4d47ce7c145669176863a4cdb695ec6be862 100644 --- a/core/modules/media_library/media_library.module +++ b/core/modules/media_library/media_library.module @@ -5,12 +5,17 @@ * Contains hook implementations for the media_library module. */ +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Template\Attribute; +use Drupal\views\Form\ViewsForm; use Drupal\views\Plugin\views\cache\CachePluginBase; +use Drupal\views\Plugin\views\query\QueryPluginBase; +use Drupal\views\Plugin\views\query\Sql; use Drupal\views\ViewExecutable; -use Drupal\Core\Template\Attribute; -use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\Render\Element; /** * Implements hook_help(). @@ -43,6 +48,27 @@ function media_library_theme() { function media_library_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) { 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', + ])); + // 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 + // parameters are prefixed and should not interfere with any other views. + // @todo Rework or remove this in https://www.drupal.org/node/2983451 + if (!empty($query)) { + $ajax_path = &$output['#attached']['drupalSettings']['views']['ajax_path']; + $parsed_url = UrlHelper::parse($ajax_path); + $query = array_merge($query, $parsed_url['query']); + $ajax_path = $parsed_url['path'] . '?' . UrlHelper::buildQuery($query); + if (isset($query['media_library_remaining'])) { + $output['#attached']['drupalSettings']['media_library']['selection_remaining'] = (int) $query['media_library_remaining']; + } + } + } } } @@ -59,7 +85,7 @@ function media_library_preprocess_media(&$variables) { 'language' => $media->language(), ]); $variables['preview_attributes'] = new Attribute(); - $variables['preview_attributes']->addClass('media-library-item__preview', 'js-click-to-select__trigger'); + $variables['preview_attributes']->addClass('media-library-item__preview', 'js-media-library-item-preview', 'js-click-to-select-trigger'); $variables['metadata_attributes'] = new Attribute(); $variables['metadata_attributes']->addClass('media-library-item__attributes'); $variables['status'] = $media->isPublished(); @@ -74,12 +100,10 @@ function media_library_preprocess_media(&$variables) { * @param \Drupal\Core\Form\FormStateInterface $form_state * The current state of the form. * - * @todo Remove in https://www.drupal.org/project/drupal/issues/2969660 + * @todo Remove in https://www.drupal.org/node/2983454 */ function media_library_form_views_form_media_library_page_alter(array &$form, FormStateInterface $form_state) { if (isset($form['media_bulk_form']) && isset($form['output'])) { - $form['#attributes']['class'][] = 'media-library-page-form'; - $form['header']['#attributes']['class'][] = 'media-library-page-form__header'; /** @var \Drupal\views\ViewExecutable $view */ $view = $form['output'][0]['#view']; foreach (Element::getVisibleChildren($form['media_bulk_form']) as $key) { @@ -93,6 +117,102 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo } } +/** + * Implements hook_form_alter(). + */ +function media_library_form_alter(array &$form, FormStateInterface $form_state, $form_id) { + $form_object = $form_state->getFormObject(); + if ($form_object instanceof ViewsForm && strpos($form_object->getBaseFormId(), 'views_form_media_library') === 0) { + $form['#attributes']['class'][] = 'media-library-views-form'; + if (isset($form['header'])) { + $form['header']['#attributes']['class'][] = 'media-library-views-form__header'; + $form['header']['media_bulk_form']['#attributes']['class'][] = 'media-library-views-form__bulk_form'; + } + } + + // Add after build to fix media library views exposed filter's submit button. + if ($form_id === 'views_exposed_form' && $form['#id'] === 'views-exposed-form-media-library-widget') { + $form['#after_build'][] = '_media_library_views_form_media_library_after_build'; + } +} + +/** + * After build callback for views form media library. + */ +function _media_library_views_form_media_library_after_build(array $form, FormStateInterface $form_state) { + // Remove .form-actions from media library views exposed filter actions + // and replace with .media-library-view--form-actions. + // + // This prevents the views exposed filter's 'Apply filter' submit button from + // being moved into the dialog's buttons. + // @see \Drupal\Core\Render\Element\Actions::processActions + // @see Drupal.behaviors.dialog.prepareDialogButtons + if (($key = array_search('form-actions', $form['actions']['#attributes']['class'])) !== FALSE) { + unset($form['actions']['#attributes']['class'][$key]); + } + $form['actions']['#attributes']['class'][] = 'media-library-view--form-actions'; + return $form; +} + +/** + * Implements hook_views_query_alter(). + * + * Alters the widget view's query to only show media that can be selected, + * based on what types are allowed in the field settings. + * + * @todo Remove in https://www.drupal.org/node/2983454 + */ +function media_library_views_query_alter(ViewExecutable $view, QueryPluginBase $query) { + if ($query instanceof Sql && $view->id() === 'media_library' && $view->current_display === 'widget') { + $types = _media_library_get_allowed_types(); + if ($types) { + $entity_type = \Drupal::entityTypeManager()->getDefinition('media'); + $group = $query->setWhereGroup(); + $query->addWhere($group, $entity_type->getDataTable() . '.' . $entity_type->getKey('bundle'), $types, 'in'); + } + } +} + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Limits the types available in the exposed filter to avoid users trying to + * filter by a type that is un-selectable. + * + * @see media_library_views_query_alter() + * + * @todo Remove in https://www.drupal.org/node/2983454 + */ +function media_library_form_views_exposed_form_alter(array &$form, FormStateInterface $form_state) { + if (isset($form['#id']) && $form['#id'] === 'views-exposed-form-media-library-widget') { + $types = _media_library_get_allowed_types(); + if ($types && isset($form['type']['#options'])) { + $keys = array_flip($types); + // Ensure that the default value (by default "All") persists. + if (isset($form['type']['#default_value'])) { + $keys[$form['type']['#default_value']] = TRUE; + } + $form['type']['#options'] = array_intersect_key($form['type']['#options'], $keys); + } + } +} + +/** + * Implements hook_field_ui_preconfigured_options_alter(). + */ +function media_library_field_ui_preconfigured_options_alter(array &$options, $field_type) { + // If the field is not an "entity_reference"-based field, bail out. + $class = \Drupal::service('plugin.manager.field.field_type')->getPluginClass($field_type); + if (!is_a($class, EntityReferenceItem::class, TRUE)) { + return; + } + + // Set the default field widget for media to be the Media library. + if (!empty($options['media'])) { + $options['media']['entity_form_display']['type'] = 'media_library_widget'; + } +} + /** * Implements hook_local_tasks_alter(). * @@ -107,3 +227,17 @@ function media_library_local_tasks_alter(&$local_tasks) { } } } + +/** + * Determines what types are allowed based on the current request. + * + * @return array + * An array of allowed types. + */ +function _media_library_get_allowed_types() { + $types = \Drupal::request()->query->get('media_library_allowed_types'); + if ($types && is_array($types)) { + return array_filter($types, 'is_string'); + } + return []; +} diff --git a/core/modules/media_library/media_library.views.inc b/core/modules/media_library/media_library.views.inc new file mode 100644 index 0000000000000000000000000000000000000000..7d1ead4c8faa5d901df7d254ced553e7ea0d981b --- /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; +} diff --git a/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php new file mode 100644 index 0000000000000000000000000000000000000000..86aa2e979539707d5db4543caff052156604b585 --- /dev/null +++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php @@ -0,0 +1,524 @@ +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) { + // Load the items for form rebuilds from the field state. + $field_state = static::getWidgetState($form['#parents'], $this->fieldDefinition->getName(), $form_state); + if (isset($field_state['items'])) { + usort($field_state['items'], [SortArray::class, 'sortByWeightElement']); + $items->setValue($field_state['items']); + } + + return parent::form($items, $form, $form_state, $get_delta); + } + + /** + * {@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('media'); + $field_name = $this->fieldDefinition->getName(); + $parents = $form['#parents']; + $id_suffix = '-' . implode('-', $parents); + $wrapper_id = $field_name . '-media-library-wrapper' . $id_suffix; + $limit_validation_errors = [array_merge($parents, [$field_name])]; + + $settings = $this->getFieldSetting('handler_settings'); + $element += [ + '#type' => 'fieldset', + '#cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(), + '#target_bundles' => isset($settings['target_bundles']) ? $settings['target_bundles'] : FALSE, + '#attributes' => [ + 'id' => $wrapper_id, + 'class' => ['media-library-widget'], + ], + '#attached' => [ + 'library' => ['media_library/widget'], + ], + ]; + + // @todo Remove in https://www.drupal.org/project/drupal/issues/2938116 + $allowed_bundles = !empty($element['#target_bundles']) ? $element['#target_bundles'] : []; + $add_url = _media_get_add_url($allowed_bundles); + if ($add_url) { + $element['create_help'] = [ + '#type' => 'container', + ]; + $element['create_help']['label'] = [ + '#type' => 'html_tag', + '#tag' => 'h4', + '#attributes' => [ + 'class' => ['label'], + ], + '#value' => $this->t('Create new media'), + ]; + $element['create_help']['description'] = [ + '#type' => 'html_tag', + '#tag' => 'div', + '#attributes' => [ + 'class' => ['description'], + ], + '#value' => $this->t('Create your media on the media add page (opens a new window), then select it in the library.', [':add_page' => $add_url]), + ]; + } + + $element['selection'] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'js-media-library-selection', + 'media-library-selection', + ], + ], + ]; + + if (empty($referenced_entities)) { + $element['empty_selection'] = [ + '#markup' => $this->t('
No media items are selected.
'), + ]; + } + else { + $element['weight_toggle'] = [ + '#type' => 'html_tag', + '#tag' => 'button', + '#value' => $this->t('Show media item weights'), + '#attributes' => [ + 'class' => [ + 'link', + 'media-library-widget__toggle-weight', + 'js-media-library-widget-toggle-weight', + ], + 'title' => $this->t('Re-order media by numerical weight instead of dragging'), + ], + ]; + } + + foreach ($referenced_entities as $delta => $media_item) { + $element['selection'][$delta] = [ + '#type' => 'container', + '#attributes' => [ + 'class' => [ + 'media-library-item', + 'js-media-library-item', + ], + ], + 'preview' => [ + '#type' => 'container', + // @todo Make the view mode configurable in https://www.drupal.org/project/drupal/issues/2971209 + 'rendered_entity' => $view_builder->view($media_item, 'media_library'), + 'remove_button' => [ + '#type' => 'submit', + '#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']], + // Prevent errors in other widgets from preventing removal. + '#limit_validation_errors' => $limit_validation_errors, + ], + ], + 'target_id' => [ + '#type' => 'hidden', + '#value' => $media_item->id(), + ], + // This hidden value can be toggled visible for accessibility. + 'weight' => [ + '#type' => 'number', + '#title' => $this->t('Weight'), + '#default_value' => $delta, + '#attributes' => [ + 'class' => [ + 'js-media-library-item-weight', + 'media-library-item__weight', + ], + ], + ], + ]; + } + + $cardinality_unlimited = ($element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED); + $remaining = $element['#cardinality'] - count($referenced_entities); + + // Inform the user of how many items are remaining. + if (!$cardinality_unlimited) { + if ($remaining) { + $cardinality_message = $this->formatPlural($remaining, 'One media item remaining.', '@count media items remaining.'); + } + else { + $cardinality_message = $this->t('The maximum number of media items have been selected.'); + } + $element['#description'] .= '