summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFrancesco Placella2018-07-24 08:35:32 (GMT)
committerFrancesco Placella2018-07-24 09:32:48 (GMT)
commitcf1e3ee787d7d9ce13861742c75906fced3dadf3 (patch)
tree4bd099798e5f0ba89166b1e429e7a63d9ab8dc4d
parent8d8925e7e1fb7edc0257f1e9285e3dece7fd9b24 (diff)
Issue #2962525 by samuel.mortenson, jrockowitz, seanB, drpal, chr.fritsch, ckrina, phenaproxima, webchick, lauriii, beautifulmind, andrewmacpherson, xjm, G√°bor Hojtsy: Create a field widget for the Media library module
(cherry picked from commit de52834d763e2f3ff12d7d75d19e6146cb49da17)
-rw-r--r--core/modules/media/media.module61
-rw-r--r--core/modules/media_library/config/install/views.view.media_library.yml177
-rw-r--r--core/modules/media_library/css/media_library.module.css36
-rw-r--r--core/modules/media_library/css/media_library.theme.css90
-rw-r--r--core/modules/media_library/js/media_library.click_to_select.es6.js6
-rw-r--r--core/modules/media_library/js/media_library.click_to_select.js6
-rw-r--r--core/modules/media_library/js/media_library.view.es6.js4
-rw-r--r--core/modules/media_library/js/media_library.view.js4
-rw-r--r--core/modules/media_library/js/media_library.widget.es6.js93
-rw-r--r--core/modules/media_library/js/media_library.widget.js67
-rw-r--r--core/modules/media_library/media_library.libraries.yml9
-rw-r--r--core/modules/media_library/media_library.module148
-rw-r--r--core/modules/media_library/media_library.views.inc22
-rw-r--r--core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php524
-rw-r--r--core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php128
-rw-r--r--core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml71
-rw-r--r--core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml38
-rw-r--r--core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_twin_media.yml29
-rw-r--r--core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml27
-rw-r--r--core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_twin_media.yml19
-rw-r--r--core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_unlimited_media.yml19
-rw-r--r--core/modules/media_library/tests/modules/media_library_test/config/install/node.type.basic_page.yml9
-rw-r--r--core/modules/media_library/tests/modules/media_library_test/media_library_test.info.yml1
-rw-r--r--core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php203
24 files changed, 1665 insertions, 126 deletions
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index 20d12b8..a95b972 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 <a href=":add_page" target="_blank">media add page</a> (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 <a href=":list_url" target="_blank">media list</a> (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 61ac136..32096a6 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 11ad56d..89b1580 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 0427143..5aab3c5 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 321ffe0..d5179c3 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 4bc041c..01cbb14 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 26e9a5b..98b85c9 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 1cde60c..9057300 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 0000000..654c2cd
--- /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 0000000..ad6fbd7
--- /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 e32dbe0..8482225 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 63b3cad..b72a4d4 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) {
@@ -94,6 +118,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().
*
* Removes tasks for the Media library if the view display no longer exists.
@@ -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 0000000..7d1ead4
--- /dev/null
+++ b/core/modules/media_library/media_library.views.inc
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * @file
+ * Contains Views integration for the media_library module.
+ */
+
+/**
+ * Implements hook_views_data().
+ */
+function media_library_views_data() {
+ $data = [];
+ $data['media']['media_library_select_form'] = [
+ 'title' => 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 0000000..86aa2e9
--- /dev/null
+++ b/core/modules/media_library/src/Plugin/Field/FieldWidget/MediaLibraryWidget.php
@@ -0,0 +1,524 @@
+<?php
+
+namespace Drupal\media_library\Plugin\Field\FieldWidget;
+
+use Drupal\Component\Serialization\Json;
+use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\SortArray;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\WidgetBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Url;
+use Drupal\media\Entity\Media;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\Validator\ConstraintViolationInterface;
+
+/**
+ * Plugin implementation of the 'media_library_widget' widget.
+ *
+ * @FieldWidget(
+ * id = "media_library_widget",
+ * label = @Translation("Media library"),
+ * description = @Translation("Allows you to select items from the media library."),
+ * field_types = {
+ * "entity_reference"
+ * },
+ * multiple_values = TRUE,
+ * )
+ *
+ * @internal
+ */
+class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInterface {
+
+ /**
+ * Entity type manager service.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * Constructs a MediaLibraryWidget widget.
+ *
+ * @param string $plugin_id
+ * The plugin_id for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+ * The definition of the field to which the widget is associated.
+ * @param array $settings
+ * The widget settings.
+ * @param array $third_party_settings
+ * Any third party settings.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * Entity type manager service.
+ */
+ public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager) {
+ parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
+ $this->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 <a href=":add_page" target="_blank">media add page</a> (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('<p>No media items are selected.</p>'),
+ ];
+ }
+ 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'] .= '<br />' . $cardinality_message;
+ }
+
+ // Add a button that will load the Media library in a modal using AJAX.
+ $element['media_library_open_button'] = [
+ '#type' => 'link',
+ '#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('view.media_library.widget', [], [
+ 'query' => [
+ 'media_library_widget_id' => $field_name . $id_suffix,
+ 'media_library_allowed_types' => $element['#target_bundles'],
+ 'media_library_remaining' => $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining,
+ ],
+ ]),
+ '#attributes' => [
+ 'class' => ['button', 'use-ajax', 'media-library-open-button'],
+ 'data-dialog-type' => 'modal',
+ 'data-dialog-options' => Json::encode([
+ 'dialogClass' => 'media-library-widget-modal',
+ 'height' => '75%',
+ 'width' => '75%',
+ 'title' => $this->t('Media library'),
+ ]),
+ ],
+ // Prevent errors in other widgets from preventing addition.
+ '#limit_validation_errors' => $limit_validation_errors,
+ '#access' => $cardinality_unlimited || $remaining > 0,
+ ];
+
+ // This hidden field and button are used to add new items to the widget.
+ $element['media_library_selection'] = [
+ '#type' => 'hidden',
+ '#attributes' => [
+ // This is used to pass the selection from the modal to the widget.
+ 'data-media-library-widget-value' => $field_name . $id_suffix,
+ ],
+ ];
+
+ // When a selection is made this hidden button is pressed to add new media
+ // items based on the "media_library_selection" value.
+ $element['media_library_update_widget'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Update widget'),
+ '#name' => $field_name . '-media-library-update' . $id_suffix,
+ '#ajax' => [
+ 'callback' => [static::class, 'updateWidget'],
+ 'wrapper' => $wrapper_id,
+ ],
+ '#attributes' => [
+ 'data-media-library-widget-update' => $field_name . $id_suffix,
+ 'class' => ['js-hide'],
+ ],
+ '#validate' => [[static::class, 'validateItems']],
+ '#submit' => [[static::class, 'updateItems']],
+ // Prevent errors in other widgets from preventing updates.
+ '#limit_validation_errors' => $limit_validation_errors,
+ ];
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
+ return isset($element['target_id']) ? $element['target_id'] : FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+ if (isset($values['selection'])) {
+ usort($values['selection'], [SortArray::class, 'sortByWeightElement']);
+ return $values['selection'];
+ }
+ return [];
+ }
+
+ /**
+ * AJAX callback to update the widget when the selection changes.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return array
+ * An array representing the updated widget.
+ */
+ public static function updateWidget(array $form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ // This callback is either invoked from the remove button or the update
+ // button, which have different nesting levels.
+ $length = end($triggering_element['#parents']) === 'remove_button' ? -4 : -1;
+ if (count($triggering_element['#array_parents']) < abs($length)) {
+ throw new \LogicException('The element that triggered the widget update was at an unexpected depth. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
+ }
+ $parents = array_slice($triggering_element['#array_parents'], 0, $length);
+ $element = NestedArray::getValue($form, $parents);
+ // Always clear the textfield selection to prevent duplicate additions.
+ $element['media_library_selection']['#value'] = '';
+ return $element;
+ }
+
+ /**
+ * Submit callback for remove buttons.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public static function removeItem(array $form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+
+ // Get the parents required to find the top-level widget element.
+ if (count($triggering_element['#array_parents']) < 4) {
+ throw new \LogicException('Expected the remove button to be more than four levels deep in the form. Triggering element parents were: ' . implode(',', $triggering_element['#array_parents']));
+ }
+ $parents = array_slice($triggering_element['#array_parents'], 0, -4);
+ // Get the delta of the item being removed.
+ $delta = array_slice($triggering_element['#array_parents'], -3, 1)[0];
+ $element = NestedArray::getValue($form, $parents);
+
+ // Get the field state.
+ $path = $element['#parents'];
+ $values = NestedArray::getValue($form_state->getValues(), $path);
+ $field_state = static::getFieldState($element, $form_state);
+
+ // Remove the item from the field state and update it.
+ if (isset($values['selection'][$delta])) {
+ array_splice($values['selection'], $delta, 1);
+ $field_state['items'] = $values['selection'];
+ static::setFieldState($element, $form_state, $field_state);
+ }
+
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Validates that newly selected items can be added to the widget.
+ *
+ * Making an invalid selection from the view should not be possible, but we
+ * still validate in case other selection methods (ex: upload) are valid.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public static function validateItems(array $form, FormStateInterface $form_state) {
+ $button = $form_state->getTriggeringElement();
+ $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+
+ $field_state = static::getFieldState($element, $form_state);
+ $media = static::getNewMediaItems($element, $form_state);
+ if (empty($media)) {
+ return;
+ }
+
+ // Check if more items were selected than we allow.
+ $cardinality_unlimited = ($element['#cardinality'] === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
+ $selection = count($field_state['items']) + count($media);
+ if (!$cardinality_unlimited && ($selection > $element['#cardinality'])) {
+ $form_state->setError($element, \Drupal::translation()->formatPlural($element['#cardinality'], 'Only one item can be selected.', 'Only @count items can be selected.'));
+ }
+
+ // Validate that each selected media is of an allowed bundle.
+ $all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
+ $bundle_labels = array_map(function ($bundle) use ($all_bundles) {
+ return $all_bundles[$bundle]['label'];
+ }, $element['#target_bundles']);
+ foreach ($media as $media_item) {
+ if ($element['#target_bundles'] && !in_array($media_item->bundle(), $element['#target_bundles'], TRUE)) {
+ $form_state->setError($element, t('The media item "@label" is not of an accepted type. Allowed types: @types', [
+ '@label' => $media_item->label(),
+ '@types' => implode(', ', $bundle_labels),
+ ]));
+ }
+ }
+ }
+
+ /**
+ * Updates the field state and flags the form for rebuild.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public static function updateItems(array $form, FormStateInterface $form_state) {
+ $button = $form_state->getTriggeringElement();
+ $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1));
+
+ $field_state = static::getFieldState($element, $form_state);
+
+ $media = static::getNewMediaItems($element, $form_state);
+ if (!empty($media)) {
+ $weight = count($field_state['items']);
+ foreach ($media as $media_item) {
+ // Any ID can be passed to the widget, so we have to check access.
+ if ($media_item->access('view')) {
+ $field_state['items'][] = [
+ 'target_id' => $media_item->id(),
+ 'weight' => $weight++,
+ ];
+ }
+ }
+ static::setFieldState($element, $form_state, $field_state);
+ }
+
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Gets newly selected media items.
+ *
+ * @param array $element
+ * The wrapping element for this widget.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return \Drupal\media\MediaInterface[]
+ * An array of selected media items.
+ */
+ protected static function getNewMediaItems(array $element, FormStateInterface $form_state) {
+ // Get the new media IDs passed to our hidden button.
+ $values = $form_state->getValues();
+ $path = $element['#parents'];
+ $value = NestedArray::getValue($values, $path);
+
+ if (!empty($value['media_library_selection'])) {
+ $ids = explode(',', $value['media_library_selection']);
+ $ids = array_filter($ids, 'is_numeric');
+ if (!empty($ids)) {
+ /** @var \Drupal\media\MediaInterface[] $media */
+ return Media::loadMultiple($ids);
+ }
+ }
+ return [];
+ }
+
+ /**
+ * Gets the field state for the widget.
+ *
+ * @param array $element
+ * The wrapping element for this widget.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return array[]
+ * An array of arrays with the following key/value pairs:
+ * - items: (array) An array of selections.
+ * - target_id: (int) A media entity ID.
+ * - weight: (int) A weight for the selection.
+ */
+ protected static function getFieldState(array $element, FormStateInterface $form_state) {
+ $widget_state = static::getWidgetState($element['#field_parents'], $element['#field_name'], $form_state);
+ $widget_state['items'] = isset($widget_state['items']) ? $widget_state['items'] : [];
+ return $widget_state;
+ }
+
+ /**
+ * Sets the field state for the widget.
+ *
+ * @param array $element
+ * The wrapping element for this widget.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ * @param array[] $field_state
+ * An array of arrays with the following key/value pairs:
+ * - items: (array) An array of selections.
+ * - target_id: (int) A media entity ID.
+ * - weight: (int) A weight for the selection.
+ */
+ protected static function setFieldState(array $element, FormStateInterface $form_state, array $field_state) {
+ static::setWidgetState($element['#field_parents'], $element['#field_name'], $form_state, $field_state);
+ }
+
+}
diff --git a/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
new file mode 100644
index 0000000..72a4b50
--- /dev/null
+++ b/core/modules/media_library/src/Plugin/views/field/MediaLibrarySelectForm.php
@@ -0,0 +1,128 @@
+<?php
+
+namespace Drupal\media_library\Plugin\views\field;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseDialogCommand;
+use Drupal\Core\Ajax\InvokeCommand;
+use Drupal\Core\Form\FormBuilderInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+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.
+ *
+ * @ViewsField("media_library_select_form")
+ *
+ * @internal
+ */
+class MediaLibrarySelectForm extends FieldPluginBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue(ResultRow $row, $field = NULL) {
+ return '<!--form-item-' . $this->options['id'] . '--' . $row->index . '-->';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render(ResultRow $values) {
+ return ViewsRenderPipelineMarkup::create($this->getValue($values));
+ }
+
+ /**
+ * Form constructor for the media library select form.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function viewsForm(array &$form, FormStateInterface $form_state) {
+ // Only add the bulk form options and buttons if there are results.
+ if (empty($this->view->result)) {
+ return;
+ }
+
+ // Render checkboxes for all rows.
+ $form[$this->options['id']]['#tree'] = TRUE;
+ foreach ($this->view->result as $row_index => $row) {
+ $entity = $this->getEntity($row);
+ $form[$this->options['id']][$row_index] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Select @label', [
+ '@label' => $entity->label(),
+ ]),
+ '#title_display' => 'invisible',
+ '#return_value' => $entity->id(),
+ ];
+ }
+
+ // @todo Remove in https://www.drupal.org/project/drupal/issues/2504115
+ // Currently the default URL for all AJAX form elements is the current URL,
+ // not the form action. This causes bugs when this form is rendered from an
+ // AJAX path like /views/ajax, which cannot process AJAX form submits.
+ $url = parse_url($form['#action'], PHP_URL_PATH);
+ $query = \Drupal::request()->query->all();
+ $query[FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
+ $form['actions']['submit']['#ajax'] = [
+ 'url' => Url::fromUserInput($url),
+ 'options' => [
+ 'query' => $query,
+ ],
+ 'callback' => [static::class, 'updateWidget'],
+ ];
+
+ $form['actions']['submit']['#value'] = $this->t('Select media');
+ $form['actions']['submit']['#field_id'] = $this->options['id'];
+ }
+
+ /**
+ * Submit handler for the media library select form.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * A command to send the selection to the current field widget.
+ */
+ public static function updateWidget(array &$form, FormStateInterface $form_state) {
+ $widget_id = \Drupal::request()->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.');
+ }
+ $field_id = $form_state->getTriggeringElement()['#field_id'];
+ $selected = array_values(array_filter($form_state->getValue($field_id, [])));
+ // 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(',', $selected)]))
+ ->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown']))
+ ->addCommand(new CloseDialogCommand());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewsFormValidate(array &$form, FormStateInterface $form_state) {
+ $selected = array_filter($form_state->getValue($this->options['id']));
+ if (empty($selected)) {
+ $form_state->setErrorByName('', $this->t('No items selected.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clickSortable() {
+ return FALSE;
+ }
+
+}
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml
new file mode 100644
index 0000000..0714863
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_form_display.node.basic_page.default.yml
@@ -0,0 +1,71 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.node.basic_page.field_twin_media
+ - field.field.node.basic_page.field_unlimited_media
+ - node.type.basic_page
+ module:
+ - media_library
+id: node.basic_page.default
+targetEntityType: node
+bundle: basic_page
+mode: default
+content:
+ created:
+ type: datetime_timestamp
+ weight: 10
+ region: content
+ settings: { }
+ third_party_settings: { }
+ field_twin_media:
+ type: media_library_widget
+ weight: 122
+ settings: { }
+ third_party_settings: { }
+ region: content
+ field_unlimited_media:
+ type: media_library_widget
+ weight: 121
+ settings: { }
+ third_party_settings: { }
+ region: content
+ promote:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 15
+ region: content
+ third_party_settings: { }
+ status:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 120
+ region: content
+ third_party_settings: { }
+ sticky:
+ type: boolean_checkbox
+ settings:
+ display_label: true
+ weight: 16
+ region: content
+ third_party_settings: { }
+ title:
+ 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.node.basic_page.default.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml
new file mode 100644
index 0000000..b747e2a
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/core.entity_view_display.node.basic_page.default.yml
@@ -0,0 +1,38 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.field.node.basic_page.field_twin_media
+ - field.field.node.basic_page.field_unlimited_media
+ - node.type.basic_page
+ module:
+ - user
+id: node.basic_page.default
+targetEntityType: node
+bundle: basic_page
+mode: default
+content:
+ field_twin_media:
+ type: entity_reference_entity_view
+ weight: 102
+ label: above
+ settings:
+ view_mode: default
+ link: false
+ third_party_settings: { }
+ region: content
+ field_unlimited_media:
+ type: entity_reference_entity_view
+ weight: 101
+ label: above
+ settings:
+ view_mode: default
+ link: false
+ third_party_settings: { }
+ region: content
+ links:
+ weight: 100
+ settings: { }
+ third_party_settings: { }
+ region: content
+hidden: { }
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_twin_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_twin_media.yml
new file mode 100644
index 0000000..7d4d8cd
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_twin_media.yml
@@ -0,0 +1,29 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.node.field_twin_media
+ - media.type.type_one
+ - media.type.type_two
+ - node.type.basic_page
+id: node.basic_page.field_twin_media
+field_name: field_twin_media
+entity_type: node
+bundle: basic_page
+label: 'Twin media'
+description: ''
+required: false
+translatable: false
+default_value: { }
+default_value_callback: ''
+settings:
+ handler: 'default:media'
+ handler_settings:
+ target_bundles:
+ type_one: type_one
+ type_two: type_two
+ sort:
+ field: _none
+ auto_create: false
+ auto_create_bundle: file
+field_type: entity_reference
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml
new file mode 100644
index 0000000..c1564e1
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.field.node.basic_page.field_unlimited_media.yml
@@ -0,0 +1,27 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.node.field_unlimited_media
+ - media.type.type_one
+ - node.type.basic_page
+id: node.basic_page.field_unlimited_media
+field_name: field_unlimited_media
+entity_type: node
+bundle: basic_page
+label: 'Unlimited media'
+description: ''
+required: false
+translatable: false
+default_value: { }
+default_value_callback: ''
+settings:
+ handler: 'default:media'
+ handler_settings:
+ target_bundles:
+ type_one: type_one
+ sort:
+ field: _none
+ auto_create: false
+ auto_create_bundle: audio
+field_type: entity_reference
diff --git a/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_twin_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_twin_media.yml
new file mode 100644
index 0000000..c7b38e6
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_twin_media.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - media
+ - node
+id: node.field_twin_media
+field_name: field_twin_media
+entity_type: node
+type: entity_reference
+settings:
+ target_type: media
+module: core
+locked: false
+cardinality: 2
+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.node.field_unlimited_media.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_unlimited_media.yml
new file mode 100644
index 0000000..a2391d2
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/field.storage.node.field_unlimited_media.yml
@@ -0,0 +1,19 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - media
+ - node
+id: node.field_unlimited_media
+field_name: field_unlimited_media
+entity_type: node
+type: entity_reference
+settings:
+ target_type: media
+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/node.type.basic_page.yml b/core/modules/media_library/tests/modules/media_library_test/config/install/node.type.basic_page.yml
new file mode 100644
index 0000000..23abb54
--- /dev/null
+++ b/core/modules/media_library/tests/modules/media_library_test/config/install/node.type.basic_page.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: true
+name: 'Basic Page'
+type: basic_page
+description: ''
+help: ''
+new_revision: true
+preview_mode: 1
+display_submitted: true
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
index 3ad1ae5..41979c8 100644
--- 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
@@ -7,4 +7,5 @@ dependencies:
- drupal:media_library
- drupal:media_test_source
- drupal:menu_ui
+ - drupal:node
- drupal:path
diff --git a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
index f7852a4..ab6e8a5 100644
--- a/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
+++ b/core/modules/media_library/tests/src/FunctionalJavascript/MediaLibraryTest.php
@@ -4,6 +4,8 @@ namespace Drupal\Tests\media_library\FunctionalJavascript;
use Drupal\FunctionalJavascriptTests\WebDriverTestBase;
use Drupal\media\Entity\Media;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
/**
* Contains Media library integration tests.
@@ -26,20 +28,26 @@ class MediaLibraryTest extends WebDriverTestBase {
// Create a few example media items for use in selection.
$media = [
'type_one' => [
- 'media_1',
- 'media_2',
+ 'Horse',
+ 'Bear',
+ 'Cat',
+ 'Dog',
],
'type_two' => [
- 'media_3',
- 'media_4',
+ 'Crocodile',
+ 'Lizard',
+ 'Snake',
+ 'Turtle',
],
];
+ $time = time();
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->setCreatedTime(++$time);
+ $entity->set($source_field, $name);
$entity->save();
}
}
@@ -47,7 +55,10 @@ class MediaLibraryTest extends WebDriverTestBase {
// Create a user who can use the Media library.
$user = $this->drupalCreateUser([
'access administration pages',
+ 'access content',
'access media overview',
+ 'edit own basic_page content',
+ 'create basic_page content',
'create media',
'delete any media',
'view media',
@@ -72,33 +83,33 @@ class MediaLibraryTest extends WebDriverTestBase {
$assert_session->linkExists('Add media');
// Verify that media from two separate types is present.
- $assert_session->pageTextContains('media_1');
- $assert_session->pageTextContains('media_3');
+ $assert_session->pageTextContains('Dog');
+ $assert_session->pageTextContains('Turtle');
// Test that users can filter by type.
$page->selectFieldOption('Media type', 'Type One');
$page->pressButton('Apply Filters');
$assert_session->assertWaitOnAjaxRequest();
- $assert_session->pageTextContains('media_2');
- $assert_session->pageTextNotContains('media_4');
+ $assert_session->pageTextContains('Dog');
+ $assert_session->pageTextNotContains('Turtle');
$page->selectFieldOption('Media type', 'Type Two');
$page->pressButton('Apply Filters');
$assert_session->assertWaitOnAjaxRequest();
- $assert_session->pageTextNotContains('media_2');
- $assert_session->pageTextContains('media_4');
+ $assert_session->pageTextNotContains('Dog');
+ $assert_session->pageTextContains('Turtle');
// Test that selecting elements as a part of bulk operations works.
$page->selectFieldOption('Media type', '- Any -');
$page->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->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');
+ $assert_session->pageTextContains('Dog');
+ $assert_session->pageTextNotContains('Cat');
$this->submitForm([], 'Delete');
- $assert_session->pageTextNotContains('media_1');
- $assert_session->pageTextContains('media_2');
+ $assert_session->pageTextNotContains('Dog');
+ $assert_session->pageTextContains('Cat');
// Test 'Select all media'.
$this->getSession()->getPage()->checkField('Select all media');
@@ -106,9 +117,9 @@ class MediaLibraryTest extends WebDriverTestBase {
$this->submitForm([], 'Apply to selected items');
$this->getSession()->getPage()->pressButton('Delete');
- $assert_session->pageTextNotContains('media_2');
- $assert_session->pageTextNotContains('media_3');
- $assert_session->pageTextNotContains('media_4');
+ $assert_session->pageTextNotContains('Cat');
+ $assert_session->pageTextNotContains('Turtle');
+ $assert_session->pageTextNotContains('Snake');
// Test empty text.
$assert_session->pageTextContains('No media available.');
@@ -121,4 +132,158 @@ class MediaLibraryTest extends WebDriverTestBase {
$assert_session->linkExists('Add media');
}
+ /**
+ * Tests that the Media library's widget works as expected.
+ */
+ public function testWidget() {
+ $assert_session = $this->assertSession();
+ $page = $this->getSession()->getPage();
+
+ // Visit a node create page.
+ $this->drupalGet('node/add/basic_page');
+
+ // Verify that both media widget instances are present.
+ $assert_session->pageTextContains('Unlimited media');
+ $assert_session->pageTextContains('Twin media');
+
+ // Add to the unlimited cardinality field.
+ $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
+ $unlimited_button->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ // Assert that only type_one media items exist, since this field only
+ // accepts items of that type.
+ $assert_session->pageTextContains('Media library');
+ $assert_session->pageTextContains('Dog');
+ $assert_session->pageTextContains('Bear');
+ $assert_session->pageTextNotContains('Turtle');
+ // Ensure that the "Select all" checkbox is not visible.
+ $this->assertFalse($assert_session->elementExists('css', '.media-library-select-all')->isVisible());
+ // Use an exposed filter.
+ $session = $this->getSession();
+ $session->getPage()->fillField('Name', 'Dog');
+ $session->getPage()->pressButton('Apply Filters');
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextContains('Dog');
+ $assert_session->pageTextNotContains('Bear');
+ // Clear the exposed filter.
+ $session->getPage()->fillField('Name', '');
+ $session->getPage()->pressButton('Apply Filters');
+ $assert_session->assertWaitOnAjaxRequest();
+ // Select the first three media items (should be Dog/Cat/Bear).
+ $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
+ $checkboxes = $page->findAll('css', $checkbox_selector);
+ $checkboxes[0]->click();
+ $checkboxes[1]->click();
+ $checkboxes[2]->click();
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
+ $assert_session->assertWaitOnAjaxRequest();
+ // Ensure that the selection completed successfully.
+ $assert_session->pageTextNotContains('Media library');
+ $assert_session->pageTextContains('Dog');
+ $assert_session->pageTextContains('Cat');
+ $assert_session->pageTextContains('Bear');
+ // Remove "Dog" (happens to be the first remove button on the page).
+ $assert_session->elementExists('css', '.media-library-item__remove')->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ $assert_session->pageTextNotContains('Dog');
+ $assert_session->pageTextContains('Cat');
+ $assert_session->pageTextContains('Bear');
+
+ // Open another Media library on the same page.
+ $twin_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_twin_media"]');
+ $twin_button->click();
+ $assert_session->assertWaitOnAjaxRequest();
+ // This field allows both media types.
+ $assert_session->pageTextContains('Media library');
+ $assert_session->pageTextContains('Dog');
+ $assert_session->pageTextContains('Turtle');
+ // Attempt to select three items - the cardinality of this field is two so
+ // the third selection should be disabled.
+ $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
+ $checkboxes = $page->findAll('css', $checkbox_selector);
+ $this->assertFalse($checkboxes[5]->hasAttribute('disabled'));
+ $checkboxes[0]->click();
+ $checkboxes[7]->click();
+ $this->assertTrue($checkboxes[5]->hasAttribute('disabled'));
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
+ $assert_session->assertWaitOnAjaxRequest();
+ // Ensure that the selection completed successfully, and we have only two
+ // media items of two different types.
+ $assert_session->pageTextNotContains('Media library');
+ $assert_session->pageTextContains('Horse');
+ $assert_session->pageTextContains('Turtle');
+ $assert_session->pageTextNotContains('Snake');
+
+ // Finally, save the form.
+ $assert_session->elementExists('css', '.js-media-library-widget-toggle-weight')->click();
+ $this->submitForm([
+ 'title[0][value]' => 'My page',
+ 'field_unlimited_media[selection][0][weight]' => '2',
+ ], 'Save');
+ $assert_session->pageTextContains('Basic Page My page has been created');
+ // We removed this item earlier.
+ $assert_session->pageTextNotContains('Dog');
+ // This item should not have been selected due to cardinality constraints.
+ $assert_session->pageTextNotContains('Snake');
+ // "Cat" should come after "Bear", since we changed the weight.
+ $assert_session->elementExists('css', '.field--name-field-unlimited-media > .field__items > .field__item:last-child:contains("Cat")');
+ // Make sure everything that was selected shows up.
+ $assert_session->pageTextContains('Cat');
+ $assert_session->pageTextContains('Bear');
+ $assert_session->pageTextContains('Horse');
+ $assert_session->pageTextContains('Turtle');
+ }
+
+ /**
+ * Tests that the widget works as expected for anonymous users.
+ */
+ public function testWidgetAnonymous() {
+ $assert_session = $this->assertSession();
+
+ $this->drupalLogout();
+
+ $role = Role::load(RoleInterface::ANONYMOUS_ID);
+ $role->revokePermission('view media');
+ $role->save();
+
+ // Verify that unprivileged users can't access the widget view.
+ $this->drupalGet('admin/content/media-widget');
+ $assert_session->responseContains('Access denied');
+
+ // Allow the anonymous user to create pages and view media.
+ $this->grantPermissions($role, [
+ 'access content',
+ 'create basic_page content',
+ 'view media',
+ ]);
+
+ // Ensure the widget works as an anonymous user.
+ $this->drupalGet('node/add/basic_page');
+
+ // Add to the unlimited cardinality field.
+ $unlimited_button = $assert_session->elementExists('css', '.media-library-open-button[href*="field_unlimited_media"]');
+ $unlimited_button->click();
+ $assert_session->assertWaitOnAjaxRequest();
+
+ // Select the first media item (should be Dog).
+ $checkbox_selector = '.media-library-view .js-click-to-select-checkbox input';
+ $checkboxes = $this->getSession()->getPage()->findAll('css', $checkbox_selector);
+ $checkboxes[0]->click();
+ $assert_session->elementExists('css', '.ui-dialog-buttonpane')->pressButton('Select media');
+ $assert_session->assertWaitOnAjaxRequest();
+
+ // Ensure that the selection completed successfully.
+ $assert_session->pageTextNotContains('Media library');
+ $assert_session->pageTextContains('Dog');
+
+ // Save the form.
+ $assert_session->elementExists('css', '.js-media-library-widget-toggle-weight')->click();
+ $this->submitForm([
+ 'title[0][value]' => 'My page',
+ 'field_unlimited_media[selection][0][weight]' => '0',
+ ], 'Save');
+ $assert_session->pageTextContains('Basic Page My page has been created');
+ $assert_session->pageTextContains('Dog');
+ }
+
}