diff --git a/core/composer.json b/core/composer.json index 7452a077766a06bc121de64d0f3701cbb37811ed..770631ddfd271a73e5c5d05c4c672d2d3d438c04 100644 --- a/core/composer.json +++ b/core/composer.json @@ -133,6 +133,7 @@ "drupal/locale": "self.version", "drupal/minimal": "self.version", "drupal/media": "self.version", + "drupal/media_library": "self.version", "drupal/menu_link_content": "self.version", "drupal/menu_ui": "self.version", "drupal/migrate": "self.version", diff --git a/core/modules/media_library/config/install/core.entity_view_mode.media.media_library.yml b/core/modules/media_library/config/install/core.entity_view_mode.media.media_library.yml new file mode 100644 index 0000000000000000000000000000000000000000..3406f026b491c2d22138287e2dbd71a2d287d543 --- /dev/null +++ b/core/modules/media_library/config/install/core.entity_view_mode.media.media_library.yml @@ -0,0 +1,12 @@ +langcode: en +status: true +dependencies: + enforced: + module: + - media_library + module: + - media +id: media.media_library +label: 'Media library' +targetEntityType: media +cache: true 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 new file mode 100644 index 0000000000000000000000000000000000000000..8c9e784e60087e537a3aa29dfd879dca0480b128 --- /dev/null +++ b/core/modules/media_library/config/install/views.view.media_library.yml @@ -0,0 +1,447 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.media_library + enforced: + module: + - media_library + module: + - media + - user +id: media_library +label: 'Media library' +module: views +description: '' +tag: '' +base_table: media_field_data +base_field: mid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: perm + options: + perm: 'access media overview' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: 'Apply Filters' + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: false + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 25 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: default + options: + grouping: { } + row_class: 'media-library-item js-click-to-select' + default_row_class: true + row: + type: fields + options: + default_field_elements: true + inline: { } + separator: '' + hide_empty: false + fields: + media_bulk_form: + id: media_bulk_form + table: media + field: media_bulk_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: js-click-to-select__checkbox + 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 + action_title: Action + include_exclude: exclude + selected_actions: { } + entity_type: media + plugin_id: bulk_form + 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 + filters: + status: + id: status + table: media_field_data + field: status + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '1' + group: 1 + exposed: true + expose: + operator_id: '' + label: 'Publishing status' + description: null + use_operator: false + operator: status_op + identifier: status + required: true + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: true + group_info: + label: Published + description: '' + identifier: status + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: + 1: + title: Published + operator: '=' + value: '1' + 2: + title: Unpublished + operator: '=' + value: '0' + plugin_id: boolean + entity_type: media + entity_field: status + name: + id: name + table: media_field_data + field: name + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: name_op + label: Name + description: '' + use_operator: false + operator: name_op + identifier: name + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: media + entity_field: name + plugin_id: string + bundle: + id: bundle + table: media_field_data + field: bundle + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: bundle_op + label: 'Media type' + description: '' + use_operator: false + operator: bundle_op + identifier: type + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: 'Media type' + description: null + identifier: bundle + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: + 1: { } + 2: { } + 3: { } + entity_type: media + entity_field: bundle + plugin_id: bundle + sorts: + created: + id: created + table: media_field_data + field: created + relationship: none + group_type: group + admin_label: '' + order: DESC + exposed: true + expose: + label: 'Newest first' + granularity: second + entity_type: media + entity_field: created + plugin_id: date + name: + id: name + table: media_field_data + field: name + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: true + expose: + label: 'Name (A-Z)' + entity_type: media + entity_field: name + plugin_id: standard + name_1: + id: name_1 + table: media_field_data + field: name + relationship: none + group_type: group + admin_label: '' + order: DESC + exposed: true + expose: + label: 'Name (Z-A)' + entity_type: media + entity_field: name + plugin_id: standard + title: Media + header: { } + footer: { } + empty: { } + 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 + 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 --git a/core/modules/media_library/config/optional/core.entity_view_display.media.audio.media_library.yml b/core/modules/media_library/config/optional/core.entity_view_display.media.audio.media_library.yml new file mode 100644 index 0000000000000000000000000000000000000000..e74e3ebde6b206148a37d6f95a4ed86140fe98be --- /dev/null +++ b/core/modules/media_library/config/optional/core.entity_view_display.media.audio.media_library.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.media_library + - field.field.media.audio.field_media_audio_file + - image.style.thumbnail + - media.type.audio + module: + - image +id: media.audio.media_library +targetEntityType: media +bundle: audio +mode: media_library +content: + thumbnail: + type: image + weight: 0 + region: content + label: hidden + settings: + image_style: thumbnail + image_link: '' + third_party_settings: { } +hidden: + created: true + field_media_audio_file: true + name: true + uid: true diff --git a/core/modules/media_library/config/optional/core.entity_view_display.media.file.media_library.yml b/core/modules/media_library/config/optional/core.entity_view_display.media.file.media_library.yml new file mode 100644 index 0000000000000000000000000000000000000000..e09e611b9550abf9f4001a7d77c488bef6388ef7 --- /dev/null +++ b/core/modules/media_library/config/optional/core.entity_view_display.media.file.media_library.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.media_library + - field.field.media.file.field_media_file + - image.style.thumbnail + - media.type.file + module: + - image +id: media.file.media_library +targetEntityType: media +bundle: file +mode: media_library +content: + thumbnail: + type: image + weight: 0 + region: content + label: hidden + settings: + image_style: thumbnail + image_link: '' + third_party_settings: { } +hidden: + created: true + field_media_file: true + name: true + uid: true diff --git a/core/modules/media_library/config/optional/core.entity_view_display.media.image.media_library.yml b/core/modules/media_library/config/optional/core.entity_view_display.media.image.media_library.yml new file mode 100644 index 0000000000000000000000000000000000000000..a916760ad99ba1898665fd0c0dcb9996f002f5c8 --- /dev/null +++ b/core/modules/media_library/config/optional/core.entity_view_display.media.image.media_library.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.media_library + - field.field.media.image.field_media_image + - image.style.medium + - media.type.image + module: + - image +id: media.image.media_library +targetEntityType: media +bundle: image +mode: media_library +content: + thumbnail: + type: image + weight: 0 + region: content + label: hidden + settings: + image_style: medium + image_link: '' + third_party_settings: { } +hidden: + created: true + field_media_image: true + name: true + uid: true diff --git a/core/modules/media_library/config/optional/core.entity_view_display.media.video.media_library.yml b/core/modules/media_library/config/optional/core.entity_view_display.media.video.media_library.yml new file mode 100644 index 0000000000000000000000000000000000000000..33d4e885555354de44e2613b5ba582342074b2c0 --- /dev/null +++ b/core/modules/media_library/config/optional/core.entity_view_display.media.video.media_library.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - core.entity_view_mode.media.media_library + - field.field.media.video.field_media_video_file + - image.style.thumbnail + - media.type.video + module: + - image +id: media.video.media_library +targetEntityType: media +bundle: video +mode: media_library +content: + thumbnail: + type: image + weight: 0 + region: content + label: hidden + settings: + image_style: thumbnail + image_link: '' + third_party_settings: { } +hidden: + created: true + field_media_video_file: true + name: true + uid: true diff --git a/core/modules/media_library/css/media_library.module.css b/core/modules/media_library/css/media_library.module.css new file mode 100644 index 0000000000000000000000000000000000000000..11ad56dd922e3fd31b30b8f5521b1df5bd47289d --- /dev/null +++ b/core/modules/media_library/css/media_library.module.css @@ -0,0 +1,60 @@ +/** +* @file media_library.module.css +*/ + +.media-library-page-form { + display: flex; + flex-wrap: wrap; +} + +.media-library-page-form > .form-actions { + flex-basis: 100%; +} + +.media-library-page-form__header > div, +.media-library-view .form--inline { + display: flex; + flex-wrap: wrap; +} + +.media-library-page-form__header { + flex-basis: 100%; +} + +.media-library-item { + position: relative; +} + +.media-library-item .js-click-to-select__trigger { + overflow: hidden; + cursor: pointer; +} + +.media-library-view .form-actions { + align-self: flex-end; +} + +.media-library-item .js-click-to-select__checkbox { + position: absolute; + display: block; + z-index: 1; + top: 5px; + right: 0; +} + +.media-library-item__status { + position: absolute; + top: 10px; + left: 2px; + pointer-events: none; +} + +.media-library-select-all { + flex-basis: 100%; +} + +@media screen and (max-width: 600px) { + .media-library-view .form-actions { + flex-basis: 100%; + } +} diff --git a/core/modules/media_library/css/media_library.theme.css b/core/modules/media_library/css/media_library.theme.css new file mode 100644 index 0000000000000000000000000000000000000000..0427143538e36cca88518bc430209ad86d519ee7 --- /dev/null +++ b/core/modules/media_library/css/media_library.theme.css @@ -0,0 +1,151 @@ +/** + * @file media_library.theme.css + * + * @todo Move into the Seven theme when this module is marked as stable. + * @see https://www.drupal.org/project/drupal/issues/2980769 + */ + +.media-library-page-form__header .form-item { + margin-right: 8px; +} + +#drupal-modal .view-header { + margin: 16px 0; +} + +.media-library-item { + justify-content: center; + vertical-align: top; + padding: 2px; + border: 1px solid #ebebeb; + margin: 16px 16px 2px 2px; + width: 180px; + background: #fff; + transition: border-color 0.2s, color 0.2s, background 0.2s; +} + +.media-library-view .form-actions { + margin: 0.75em 0; +} + +.media-library-item .field--name-thumbnail { + background-color: #ebebeb; + margin: 2px; + overflow: hidden; + text-align: center; +} + +.media-library-item .field--name-thumbnail img { + height: 180px; + object-fit: contain; + object-position: center center; +} + +.media-library-item.is-hover, +.media-library-item.checked, +.media-library-item.is-focus { + border-color: #40b6ff; + border-width: 3px; + border-radius: 3px; + margin: 14px 14px 0 0; +} + +.media-library-item.checked { + border-color: #0076c0; +} + +.media-library-item .js-click-to-select__checkbox input { + width: 30px; + height: 30px; +} + +.media-library-item .js-click-to-select__checkbox .form-item { + margin: 0; +} + +.media-library-item__preview { + padding-bottom: 44px; +} + +.media-library-item__status { + color: #e4e4e4; + font-style: italic; + background: #666; + padding: 5px 10px; + font-size: 12px; +} + +.media-library-item .views-field-operations { + height: 30px; +} + +.media-library-item .views-field-operations .dropbutton-wrapper { + display: inline-block; + position: absolute; + right: 5px; + bottom: 5px; +} + +.media-library-item__attributes { + position: absolute; + bottom: 0; + display: block; + padding: 10px; + max-width: calc(100% - 20px); + max-height: calc(100% - 60px); + overflow: hidden; + background: white; +} + +.media-library-item__name { + font-size: 14px; +} + +.media-library-item__name a { + display: block; + text-decoration: underline; + margin: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.media-library-item__attributes:hover .media-library-item__name a, +.media-library-item__name a:focus, +.media-library-item.is-focus .media-library-item__name a, +.media-library-item.checked .media-library-item__name a { + white-space: normal; +} + +.media-library-item__name a:focus { + border: 2px solid; + margin: 0; +} + +.media-library-item__type { + font-size: 12px; + color: #696969; +} + +.media-library-select-all { + margin: 10px 0 10px 0; +} + +.media-library-select-all input { + margin-right: 10px; +} + +@media screen and (max-width: 600px) { + .media-library-item { + width: 150px; + } + .media-library-item .field--name-thumbnail img { + height: 150px; + width: 150px; + } + .media-library-item .views-field-operations .dropbutton-wrapper { + position: relative; + right: 0; + border: 0; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..321ffe0c73a9f9f056123d10fad095d832d2a1e4 --- /dev/null +++ b/core/modules/media_library/js/media_library.click_to_select.es6.js @@ -0,0 +1,31 @@ +/** + * @file media_library.click_to_select.es6.js + */ + +(($, Drupal) => { + /** + * Allows users to select an element which checks a hidden checkbox. + */ + Drupal.behaviors.ClickToSelect = { + attach(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. + event.preventDefault(); + // 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'); + $input.prop('checked', !$input.prop('checked')).trigger('change'); + }); + $('.js-click-to-select__checkbox input', context) + .once('media-library-click-to-select') + .on('change', ({ currentTarget }) => { + $(currentTarget) + .closest('.js-click-to-select') + .toggleClass('checked', $(currentTarget).prop('checked')); + }); + }, + }; +})(jQuery, Drupal); 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 new file mode 100644 index 0000000000000000000000000000000000000000..4bc041c43e56a72a8047b1bc413d4e3d445e4cbf --- /dev/null +++ b/core/modules/media_library/js/media_library.click_to_select.js @@ -0,0 +1,24 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(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) { + event.preventDefault(); + + 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) { + var currentTarget = _ref.currentTarget; + + $(currentTarget).closest('.js-click-to-select').toggleClass('checked', $(currentTarget).prop('checked')); + }); + } + }; +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/modules/media_library/js/media_library.view.es6.js b/core/modules/media_library/js/media_library.view.es6.js new file mode 100644 index 0000000000000000000000000000000000000000..26e9a5b2635ef05e7276c636a063207d9383f290 --- /dev/null +++ b/core/modules/media_library/js/media_library.view.es6.js @@ -0,0 +1,58 @@ +/** + * @file media_library.view.es6.js + */ +(($, Drupal) => { + /** + * Adds hover effect to media items. + */ + 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') + .on('mouseover mouseout', ({ currentTarget, type }) => { + $(currentTarget).closest('.media-library-item').toggleClass('is-hover', type === 'mouseover'); + }); + }, + }; + + /** + * Adds focus effect to media items. + */ + Drupal.behaviors.MediaLibraryFocus = { + attach(context) { + $('.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'); + }); + }, + }; + + /** + * Adds checkbox to select all items in the library. + */ + Drupal.behaviors.MediaLibrarySelectAll = { + attach(context) { + const $view = $('.media-library-view', context).once('media-library-select-all'); + if ($view.length && $view.find('.media-library-item').length) { + const $checkbox = $('') + .on('click', ({ currentTarget }) => { + // Toggle all checkboxes. + const $checkboxes = $(currentTarget) + .closest('.media-library-view') + .find('.media-library-item input[type="checkbox"]'); + $checkboxes + .prop('checked', $(currentTarget).prop('checked')) + .trigger('change'); + // Announce the selection. + const announcement = $(currentTarget).prop('checked') ? + Drupal.t('Zero items selected') : + Drupal.t('All @count items selected', { '@count': $checkboxes.length }); + Drupal.announce(announcement); + }); + const $label = $('') + .text(Drupal.t('Select all media')); + $label.prepend($checkbox); + $view.find('.media-library-item').first().before($label); + } + }, + }; +})(jQuery, Drupal); diff --git a/core/modules/media_library/js/media_library.view.js b/core/modules/media_library/js/media_library.view.js new file mode 100644 index 0000000000000000000000000000000000000000..1cde60c3acfb633d4c792b2d70cba731a19d0e02 --- /dev/null +++ b/core/modules/media_library/js/media_library.view.js @@ -0,0 +1,50 @@ +/** +* DO NOT EDIT THIS FILE. +* See the following change record for more information, +* https://www.drupal.org/node/2815083 +* @preserve +**/ + +(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) { + var currentTarget = _ref.currentTarget, + type = _ref.type; + + $(currentTarget).closest('.media-library-item').toggleClass('is-hover', type === 'mouseover'); + }); + } + }; + + 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) { + var currentTarget = _ref2.currentTarget, + type = _ref2.type; + + $(currentTarget).closest('.media-library-item').toggleClass('is-focus', type === 'focus'); + }); + } + }; + + Drupal.behaviors.MediaLibrarySelectAll = { + attach: function attach(context) { + var $view = $('.media-library-view', context).once('media-library-select-all'); + if ($view.length && $view.find('.media-library-item').length) { + var $checkbox = $('').on('click', function (_ref3) { + var currentTarget = _ref3.currentTarget; + + var $checkboxes = $(currentTarget).closest('.media-library-view').find('.media-library-item input[type="checkbox"]'); + $checkboxes.prop('checked', $(currentTarget).prop('checked')).trigger('change'); + + var announcement = $(currentTarget).prop('checked') ? Drupal.t('Zero items selected') : Drupal.t('All @count items selected', { '@count': $checkboxes.length }); + Drupal.announce(announcement); + }); + var $label = $('').text(Drupal.t('Select all media')); + $label.prepend($checkbox); + $view.find('.media-library-item').first().before($label); + } + } + }; +})(jQuery, Drupal); \ No newline at end of file diff --git a/core/modules/media_library/media_library.info.yml b/core/modules/media_library/media_library.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..9f7aa912e405165a3950bff6b0aec04b2b5b5f75 --- /dev/null +++ b/core/modules/media_library/media_library.info.yml @@ -0,0 +1,10 @@ +name: 'Media library' +type: module +description: 'Provides a library for re-using Media Items.' +package: Core (Experimental) +version: VERSION +core: 8.x +dependencies: + - drupal:media + - drupal:views + - drupal:user diff --git a/core/modules/media_library/media_library.install b/core/modules/media_library/media_library.install new file mode 100644 index 0000000000000000000000000000000000000000..79a97868b7b646c52b9de81703680165d8e9d739 --- /dev/null +++ b/core/modules/media_library/media_library.install @@ -0,0 +1,49 @@ +getDisplay('media_page_list'); + if (!empty($display)) { + $display['display_options']['path'] = 'admin/content/media-table'; + unset($display['display_options']['menu']); + $view->trustData()->save(); + } + } +} + +/** + * Implements hook_uninstall(). + */ +function media_library_uninstall() { + // Restore the path to the original media view. + /** @var \Drupal\views\Entity\View $view */ + if ($view = View::load('media')) { + $display = &$view->getDisplay('media_page_list'); + if (!empty($display)) { + $display['display_options']['path'] = 'admin/content/media'; + $display['display_options']['menu'] = [ + 'type' => 'tab', + 'title' => 'Media', + 'description' => '', + 'expanded' => FALSE, + 'parent' => '', + 'weight' => 0, + 'context' => '0', + 'menu_name' => 'main', + ]; + $view->trustData()->save(); + } + } +} diff --git a/core/modules/media_library/media_library.libraries.yml b/core/modules/media_library/media_library.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..e32dbe074b496aa5b19cb47fede291b5d86ce54b --- /dev/null +++ b/core/modules/media_library/media_library.libraries.yml @@ -0,0 +1,25 @@ +style: + version: VERSION + css: + component: + css/media_library.module.css: {} + theme: + css/media_library.theme.css: {} + +click_to_select: + version: VERSION + js: + js/media_library.click_to_select.js: {} + dependencies: + - core/drupal + - core/jquery.once + +view: + version: VERSION + js: + js/media_library.view.js: {} + dependencies: + - media_library/style + - media_library/click_to_select + - core/drupal.announce + - core/jquery.once diff --git a/core/modules/media_library/media_library.links.action.yml b/core/modules/media_library/media_library.links.action.yml new file mode 100644 index 0000000000000000000000000000000000000000..6a37769cf17fa642ebc3dc5793c83229b041dbcb --- /dev/null +++ b/core/modules/media_library/media_library.links.action.yml @@ -0,0 +1,5 @@ +media_library.add: + route_name: entity.media.add_page + title: 'Add media' + appears_on: + - view.media_library.page diff --git a/core/modules/media_library/media_library.links.task.yml b/core/modules/media_library/media_library.links.task.yml new file mode 100644 index 0000000000000000000000000000000000000000..757ecd853249e3ee513917e99ab89d92092108e0 --- /dev/null +++ b/core/modules/media_library/media_library.links.task.yml @@ -0,0 +1,10 @@ +media_library.grid: + title: 'Grid' + parent_id: entity.media.collection + route_name: entity.media.collection + weight: 10 +media_library.table: + title: 'Table' + parent_id: entity.media.collection + route_name: view.media.media_page_list + weight: 20 diff --git a/core/modules/media_library/media_library.module b/core/modules/media_library/media_library.module new file mode 100644 index 0000000000000000000000000000000000000000..63b3cadb96a8e4f56030b9829f6a47f43be969ba --- /dev/null +++ b/core/modules/media_library/media_library.module @@ -0,0 +1,109 @@ +' . t('About') . ''; + $output .= '

' . t('The Media library module overrides the /admin/content/media view to provide a rich visual interface for performing administrative operations on media. For more information, see the online documentation for the Media library module.', [':media' => 'https://www.drupal.org/docs/8/core/modules/media']) . '

'; + return $output; + } +} + +/** + * Implements hook_theme(). + */ +function media_library_theme() { + return [ + 'media__media_library' => [ + 'base hook' => 'media', + ], + ]; +} + +/** + * Implements hook_views_post_render(). + */ +function media_library_views_post_render(ViewExecutable $view, &$output, CachePluginBase $cache) { + if ($view->id() === 'media_library') { + $output['#attached']['library'][] = 'media_library/view'; + } +} + +/** + * Implements hook_preprocess_media(). + */ +function media_library_preprocess_media(&$variables) { + if ($variables['view_mode'] === 'media_library') { + /** @var \Drupal\media\MediaInterface $media */ + $media = $variables['media']; + $variables['#cache']['contexts'][] = 'user.permissions'; + $rel = $media->access('edit') ? 'edit-form' : 'canonical'; + $variables['url'] = $media->toUrl($rel, [ + 'language' => $media->language(), + ]); + $variables['preview_attributes'] = new Attribute(); + $variables['preview_attributes']->addClass('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(); + } +} + +/** + * Alter the bulk form to add a more accessible label. + * + * @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. + * + * @todo Remove in https://www.drupal.org/project/drupal/issues/2969660 + */ +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) { + if (isset($view->result[$key])) { + $media = $view->field['media_bulk_form']->getEntity($view->result[$key]); + $form['media_bulk_form'][$key]['#title'] = t('Select @label', [ + '@label' => $media->label(), + ]); + } + } + } +} + +/** + * Implements hook_local_tasks_alter(). + * + * Removes tasks for the Media library if the view display no longer exists. + */ +function media_library_local_tasks_alter(&$local_tasks) { + /** @var \Symfony\Component\Routing\RouteCollection $route_collection */ + $route_collection = \Drupal::service('router')->getRouteCollection(); + foreach (['media_library.grid', 'media_library.table'] as $key) { + if (isset($local_tasks[$key]) && !$route_collection->get($local_tasks[$key]['route_name'])) { + unset($local_tasks[$key]); + } + } +} diff --git a/core/modules/media_library/templates/media--media-library.html.twig b/core/modules/media_library/templates/media--media-library.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..a55024a22d2df1c3384fc71c245abbbec2e72c7b --- /dev/null +++ b/core/modules/media_library/templates/media--media-library.html.twig @@ -0,0 +1,50 @@ +{# +/** + * @file + * Default theme implementation to present a media entity in the media library. + * + * Available variables: + * - media: The entity with limited access to object properties and methods. + * Only method names starting with "get", "has", or "is" and a few common + * methods such as "id", "label", and "bundle" are available. For example: + * - entity.getEntityTypeId() will return the entity type ID. + * - entity.hasField('field_example') returns TRUE if the entity includes + * field_example. (This does not indicate the presence of a value in this + * field.) + * Calling other methods, such as entity.delete(), will result in an exception. + * See \Drupal\Core\Entity\EntityInterface for a full list of methods. + * - name: Name of the media. + * - content: Media content. + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * - view_mode: View mode; for example, "teaser" or "full". + * - attributes: HTML attributes for the containing element. + * - title_attributes: Same as attributes, except applied to the main title + * tag that appears in the template. + * - url: Direct URL of the media. + * - preview_attributes: HTML attributes for the preview wrapper. + * - metadata_attributes: HTML attributes for the expandable metadata area. + * - status: Whether or not the Media is published. + * + * @see template_preprocess_media() + * + * @ingroup themeable + */ +#} + + {% if content %} + + {{ content|without('name') }} + + {% if not status %} +
{{ "unpublished" | t }}
+ {% endif %} + + + + {% endif %} + diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_one.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_one.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..212daf81879608a79d7d404be27ec4fc0611e9ac --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_one.default.yml @@ -0,0 +1,43 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.type_one.field_media_test + - media.type.type_one +id: media.type_one.default +targetEntityType: media +bundle: type_one +mode: default +content: + created: + type: datetime_timestamp + weight: 10 + region: content + settings: { } + third_party_settings: { } + field_media_test: + settings: + size: 60 + placeholder: '' + third_party_settings: { } + type: string_textfield + weight: 11 + region: content + name: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + region: content + third_party_settings: { } +hidden: { } diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_two.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_two.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..fabd13b0297b24082152efa1374ad150e520a509 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.media.type_two.default.yml @@ -0,0 +1,43 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.type_two.field_media_test_1 + - media.type.type_two +id: media.type_two.default +targetEntityType: media +bundle: type_two +mode: default +content: + created: + type: datetime_timestamp + weight: 10 + region: content + settings: { } + third_party_settings: { } + field_media_test_1: + settings: + size: 60 + placeholder: '' + third_party_settings: { } + type: string_textfield + weight: 11 + region: content + name: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + region: content + third_party_settings: { } +hidden: { } diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_one.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_one.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..95670b3557bb170a2294d85d1b391319a1a2a36b --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_one.default.yml @@ -0,0 +1,50 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.type_one.field_media_test + - image.style.thumbnail + - media.type.type_one + module: + - image + - user +id: media.type_one.default +targetEntityType: media +bundle: type_one +mode: default +content: + created: + label: hidden + type: timestamp + weight: 0 + region: content + settings: + date_format: medium + custom_date_format: '' + timezone: '' + third_party_settings: { } + field_media_test: + label: above + settings: + link_to_entity: true + third_party_settings: { } + type: string + weight: 6 + region: content + thumbnail: + type: image + weight: 5 + label: hidden + settings: + image_style: thumbnail + image_link: '' + region: content + third_party_settings: { } + uid: + label: hidden + type: author + weight: 0 + region: content + settings: { } + third_party_settings: { } +hidden: { } diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_two.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_two.default.yml new file mode 100644 index 0000000000000000000000000000000000000000..a884fee4ce93cd2d3651f99f7b72af4840c6c262 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.media.type_two.default.yml @@ -0,0 +1,50 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.type_two.field_media_test_1 + - image.style.thumbnail + - media.type.type_two + module: + - image + - user +id: media.type_two.default +targetEntityType: media +bundle: type_two +mode: default +content: + created: + label: hidden + type: timestamp + weight: 0 + region: content + settings: + date_format: medium + custom_date_format: '' + timezone: '' + third_party_settings: { } + field_media_test_1: + label: above + settings: + link_to_entity: false + third_party_settings: { } + type: string + weight: 6 + region: content + thumbnail: + type: image + weight: 5 + label: hidden + settings: + image_style: thumbnail + image_link: '' + region: content + third_party_settings: { } + uid: + label: hidden + type: author + weight: 0 + region: content + settings: { } + third_party_settings: { } +hidden: { } diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_one.field_media_test.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_one.field_media_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..f19207c9e1d31be1e17a9d02dc8106b3190ea849 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_one.field_media_test.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_test + - media.type.type_one +id: media.type_one.field_media_test +field_name: field_media_test +entity_type: media +bundle: type_one +label: field_media_test +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_two.field_media_test_1.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_two.field_media_test_1.yml new file mode 100644 index 0000000000000000000000000000000000000000..5b093caf7bb3e50c8846bd6ec0b82a541d70b17a --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.media.type_two.field_media_test_1.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_test_1 + - media.type.type_two +id: media.type_two.field_media_test_1 +field_name: field_media_test_1 +entity_type: media +bundle: type_two +label: field_media_test_1 +description: '' +required: false +translatable: true +default_value: { } +default_value_callback: '' +settings: { } +field_type: string diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test.yml new file mode 100644 index 0000000000000000000000000000000000000000..40a77916ec7fbcbfa73d8c84479f5757c1630490 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media.field_media_test +field_name: field_media_test +entity_type: media +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test_1.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test_1.yml new file mode 100644 index 0000000000000000000000000000000000000000..73b11058e4f11f35fa00df76ef9e9a80e97176f7 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.media.field_media_test_1.yml @@ -0,0 +1,20 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media.field_media_test_1 +field_name: field_media_test_1 +entity_type: media +type: string +settings: + max_length: 255 + is_ascii: false + case_sensitive: false +module: core +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_one.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_one.yml new file mode 100644 index 0000000000000000000000000000000000000000..1f72b8beb6d13d8f66f53d9cf9b395420cb8d243 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_one.yml @@ -0,0 +1,16 @@ +langcode: en +status: true +dependencies: + module: + - media + - media_test_source +id: type_one +label: 'Type One' +description: '' +source: test +queue_thumbnail_downloads: false +new_revision: false +source_configuration: + source_field: field_media_test + test_config_value: 'This is default value.' +field_map: { } diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_two.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_two.yml new file mode 100644 index 0000000000000000000000000000000000000000..13b549c6acb8d1fbe1c8d75b8111fa44ec685e9d --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/config/install/media.type.type_two.yml @@ -0,0 +1,16 @@ +langcode: en +status: true +dependencies: + module: + - media + - media_test_source +id: type_two +label: 'Type Two' +description: '' +source: test +queue_thumbnail_downloads: false +new_revision: false +source_configuration: + source_field: field_media_test_1 + test_config_value: 'This is default value.' +field_map: { } diff --git a/core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml b/core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..3ad1ae5f46ea498431a760d02abfed82069c12b9 --- /dev/null +++ b/core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml @@ -0,0 +1,10 @@ +name: 'Media library test' +type: module +description: 'Test module for Media library.' +package: Testing +core: 8.x +dependencies: + - drupal:media_library + - drupal:media_test_source + - drupal:menu_ui + - drupal:path diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..f8604e644bf997c07622f319fd7e1392f609a1ab --- /dev/null +++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php @@ -0,0 +1,102 @@ + [ + 'media_1', + 'media_2', + ], + 'type_two' => [ + 'media_3', + 'media_4', + ], + ]; + + foreach ($media as $type => $names) { + foreach ($names as $name) { + $entity = Media::create(['name' => $name, 'bundle' => $type]); + $source_field = $type === 'type_one' ? 'field_media_test' : 'field_media_test_1'; + $entity->set($source_field, $this->randomString()); + $entity->save(); + } + } + + // Create a user who can use the Media library. + $user = $this->drupalCreateUser([ + 'access administration pages', + 'access media overview', + 'create media', + 'delete any media', + 'view media', + ]); + $this->drupalLogin($user); + $this->drupalPlaceBlock('local_tasks_block'); + $this->drupalPlaceBlock('local_actions_block'); + } + + /** + * Tests that the Media library's administration page works as expected. + */ + public function testAdministrationPage() { + $assert_session = $this->assertSession(); + + // Visit the administration page. + $this->drupalGet('admin/content/media'); + + // Verify that the "Add media" link is present. + $assert_session->linkExists('Add media'); + + // Verify that media from two separate types is present. + $assert_session->pageTextContains('media_1'); + $assert_session->pageTextContains('media_3'); + + // Test that users can filter by type. + $this->getSession()->getPage()->selectFieldOption('Media type', 'Type One'); + $this->getSession()->getPage()->pressButton('Apply Filters'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextContains('media_2'); + $assert_session->pageTextNotContains('media_4'); + $this->getSession()->getPage()->selectFieldOption('Media type', 'Type Two'); + $this->getSession()->getPage()->pressButton('Apply Filters'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->pageTextNotContains('media_2'); + $assert_session->pageTextContains('media_4'); + + // Test that selecting elements as a part of bulk operations works. + $this->getSession()->getPage()->selectFieldOption('Media type', '- Any -'); + $this->getSession()->getPage()->pressButton('Apply Filters'); + $assert_session->assertWaitOnAjaxRequest(); + // This tests that anchor tags clicked inside the preview are suppressed. + $this->getSession()->executeScript('jQuery(".js-click-to-select__trigger a")[0].click()'); + $this->submitForm([], 'Apply to selected items'); + $assert_session->pageTextContains('media_1'); + $assert_session->pageTextNotContains('media_2'); + $this->submitForm([], 'Delete'); + $assert_session->pageTextNotContains('media_1'); + $assert_session->pageTextContains('media_2'); + } + +}