first commit

This commit is contained in:
2020-06-08 23:57:36 +02:00
commit 6277454f7a
16057 changed files with 1715382 additions and 0 deletions
@@ -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
@@ -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
@@ -0,0 +1,17 @@
langcode: en
status: true
dependencies:
enforced:
module:
- media_library
name: media_library
label: 'Media Library thumbnail (220×220)'
effects:
75b076a8-1234-4b42-85db-bf377c4d8d5f:
uuid: 75b076a8-1234-4b42-85db-bf377c4d8d5f
id: image_scale
weight: 0
data:
width: 220
height: 220
upscale: false
@@ -0,0 +1 @@
advanced_ui: false
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
field.widget.settings.media_library_widget:
type: mapping
label: 'Media library widget settings'
mapping:
media_types:
type: sequence
label: 'Allowed media types, in display order'
sequence:
type: string
label: 'Media type ID'
media_library.settings:
type: config_object
label: 'Media library settings'
mapping:
advanced_ui:
type: boolean
label: 'Enable advanced UI'
@@ -0,0 +1,53 @@
/**
* @file media_library.click_to_select.es6.js
*/
(($, Drupal) => {
/**
* Allows users to select an element which checks a hidden checkbox.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior for selecting media library item.
*/
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')
// Adds checked class to the click-to-select element.
.on('change', ({ currentTarget }) => {
$(currentTarget)
.closest('.js-click-to-select')
.toggleClass('checked', $(currentTarget).prop('checked'));
})
// Adds is-focus class to the click-to-select element.
.on('focus blur', ({ currentTarget, type }) => {
$(currentTarget)
.closest('.js-click-to-select')
.toggleClass('is-focus', type === 'focus');
});
// Adds hover class to the click-to-select element.
$('.js-click-to-select-trigger, .js-click-to-select-checkbox', context)
.once('media-library-click-to-select-hover')
.on('mouseover mouseout', ({ currentTarget, type }) => {
$(currentTarget)
.closest('.js-click-to-select')
.toggleClass('is-hover', type === 'mouseover');
});
},
};
})(jQuery, Drupal);
@@ -0,0 +1,37 @@
/**
* 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'));
}).on('focus blur', function (_ref2) {
var currentTarget = _ref2.currentTarget,
type = _ref2.type;
$(currentTarget).closest('.js-click-to-select').toggleClass('is-focus', type === 'focus');
});
$('.js-click-to-select-trigger, .js-click-to-select-checkbox', context).once('media-library-click-to-select-hover').on('mouseover mouseout', function (_ref3) {
var currentTarget = _ref3.currentTarget,
type = _ref3.type;
$(currentTarget).closest('.js-click-to-select').toggleClass('is-hover', type === 'mouseover');
});
}
};
})(jQuery, Drupal);
@@ -0,0 +1,423 @@
/**
* @file media_library.ui.es6.js
*/
(($, Drupal, window) => {
/**
* Wrapper object for the current state of the media library.
*/
Drupal.MediaLibrary = {
/**
* When a user interacts with the media library we want the selection to
* persist as long as the media library modal is opened. We temporarily
* store the selected items while the user filters the media library view or
* navigates to different tabs.
*/
currentSelection: [],
};
/**
* Command to update the current media library selection.
*
* @param {Drupal.Ajax} [ajax]
* The Drupal Ajax object.
* @param {object} response
* Object holding the server response.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function(
ajax,
response,
status,
) {
Object.values(response.mediaIds).forEach(value => {
Drupal.MediaLibrary.currentSelection.push(value);
});
};
/**
* Load media library content through AJAX.
*
* Standard AJAX links (using the 'use-ajax' class) replace the entire library
* dialog. When navigating to a media type through the vertical tabs, we only
* want to load the changed library content. This is not only more efficient,
* but also provides a more accessible user experience for screen readers.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to vertical tabs in the media library.
*
* @todo Remove when the AJAX system adds support for replacing a specific
* selector via a link.
* https://www.drupal.org/project/drupal/issues/3026636
*/
Drupal.behaviors.MediaLibraryTabs = {
attach(context) {
const $menu = $('.js-media-library-menu');
$menu
.find('a', context)
.once('media-library-menu-item')
.on('keypress', e => {
// The AJAX link has the button role, so we need to make sure the link
// is also triggered when pressing the spacebar.
if (e.which === 32) {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).trigger('click');
}
})
.on('click', e => {
e.preventDefault();
e.stopPropagation();
// Replace the library content.
const ajaxObject = Drupal.ajax({
wrapper: 'media-library-content',
url: e.currentTarget.href,
dialogType: 'ajax',
progress: {
type: 'fullscreen',
message: Drupal.t('Please wait...'),
},
});
// Override the AJAX success callback to shift focus to the media
// library content.
ajaxObject.success = function(response, status) {
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
}
if (this.progress.object) {
this.progress.object.stopMonitoring();
}
$(this.element).prop('disabled', false);
// Execute the AJAX commands.
Object.keys(response || {}).forEach(i => {
if (response[i].command && this.commands[response[i].command]) {
this.commands[response[i].command](this, response[i], status);
}
});
// Set focus to the first tabbable element in the media library
// content.
$('#media-library-content :tabbable:first').focus();
// Remove any response-specific settings so they don't get used on
// the next call by mistake.
this.settings = null;
};
ajaxObject.execute();
// Set the selected tab.
$menu.find('.active-tab').remove();
$menu.find('a').removeClass('active');
$(e.currentTarget)
.addClass('active')
.html(
Drupal.t(
'<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span><span class="active-tab visually-hidden"> (selected)</span>',
{ '@title': $(e.currentTarget).data('title') },
),
);
// Announce the updated content.
Drupal.announce(
Drupal.t('Showing @title media.', {
'@title': $(e.currentTarget).data('title'),
}),
);
});
},
};
/**
* Load media library displays through AJAX.
*
* Standard AJAX links (using the 'use-ajax' class) replace the entire library
* dialog. When navigating to a media library views display, we only want to
* load the changed views display content. This is not only more efficient,
* but also provides a more accessible user experience for screen readers.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to vertical tabs in the media library.
*
* @todo Remove when the AJAX system adds support for replacing a specific
* selector via a link.
* https://www.drupal.org/project/drupal/issues/3026636
*/
Drupal.behaviors.MediaLibraryViewsDisplay = {
attach(context) {
const $view = $(context).hasClass('.js-media-library-view')
? $(context)
: $('.js-media-library-view', context);
// Add a class to the view to allow it to be replaced via AJAX.
// @todo Remove the custom ID when the AJAX system allows replacing
// elements by selector.
// https://www.drupal.org/project/drupal/issues/2821793
$view
.closest('.views-element-container')
.attr('id', 'media-library-view');
// We would ideally use a generic JavaScript specific class to detect the
// display links. Since we have no good way of altering display links yet,
// this is the best we can do for now.
// @todo Add media library specific classes and data attributes to the
// media library display links when we can alter display links.
// https://www.drupal.org/project/drupal/issues/3036694
$('.views-display-link-widget, .views-display-link-widget_table', context)
.once('media-library-views-display-link')
.on('click', e => {
e.preventDefault();
e.stopPropagation();
const $link = $(e.currentTarget);
// Add a loading and display announcement for screen reader users.
let loadingAnnouncement = '';
let displayAnnouncement = '';
let focusSelector = '';
if ($link.hasClass('views-display-link-widget')) {
loadingAnnouncement = Drupal.t('Loading grid view.');
displayAnnouncement = Drupal.t('Changed to grid view.');
focusSelector = '.views-display-link-widget';
} else if ($link.hasClass('views-display-link-widget_table')) {
loadingAnnouncement = Drupal.t('Loading table view.');
displayAnnouncement = Drupal.t('Changed to table view.');
focusSelector = '.views-display-link-widget_table';
}
// Replace the library view.
const ajaxObject = Drupal.ajax({
wrapper: 'media-library-view',
url: e.currentTarget.href,
dialogType: 'ajax',
progress: {
type: 'fullscreen',
message: loadingAnnouncement || Drupal.t('Please wait...'),
},
});
// Override the AJAX success callback to announce the updated content
// to screen readers.
if (displayAnnouncement || focusSelector) {
const success = ajaxObject.success;
ajaxObject.success = function(response, status) {
success.bind(this)(response, status);
// The AJAX link replaces the whole view, including the clicked
// link. Move the focus back to the clicked link when the view is
// replaced.
if (focusSelector) {
$(focusSelector).focus();
}
// Announce the new view is loaded to screen readers.
if (displayAnnouncement) {
Drupal.announce(displayAnnouncement);
}
};
}
ajaxObject.execute();
// Announce the new view is being loaded to screen readers.
// @todo Replace custom announcement when
// https://www.drupal.org/project/drupal/issues/2973140 is in.
if (loadingAnnouncement) {
Drupal.announce(loadingAnnouncement);
}
});
},
};
/**
* Update the media library selection when loaded or media items are selected.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to select media items.
*/
Drupal.behaviors.MediaLibraryItemSelection = {
attach(context, settings) {
const $form = $(
'.js-media-library-views-form, .js-media-library-add-form',
context,
);
const currentSelection = Drupal.MediaLibrary.currentSelection;
if (!$form.length) {
return;
}
const $mediaItems = $(
'.js-media-library-item input[type="checkbox"]',
$form,
);
/**
* Disable media items.
*
* @param {jQuery} $items
* A jQuery object representing the media items that should be disabled.
*/
function disableItems($items) {
$items
.prop('disabled', true)
.closest('.js-media-library-item')
.addClass('media-library-item--disabled');
}
/**
* Enable media items.
*
* @param {jQuery} $items
* A jQuery object representing the media items that should be enabled.
*/
function enableItems($items) {
$items
.prop('disabled', false)
.closest('.js-media-library-item')
.removeClass('media-library-item--disabled');
}
/**
* Update the number of selected items in the button pane.
*
* @param {number} remaining
* The number of remaining slots.
*/
function updateSelectionCount(remaining) {
// When the remaining number of items is a negative number, we allow an
// unlimited number of items. In that case we don't want to show the
// number of remaining slots.
const selectItemsText =
remaining < 0
? Drupal.formatPlural(
currentSelection.length,
'1 item selected',
'@count items selected',
)
: Drupal.formatPlural(
remaining,
'@selected of @count item selected',
'@selected of @count items selected',
{
'@selected': currentSelection.length,
},
);
// The selected count div could have been created outside of the
// context, so we unfortunately can't use context here.
$('.js-media-library-selected-count').html(selectItemsText);
}
// Update the selection array and the hidden form field when a media item
// is selected.
$mediaItems.once('media-item-change').on('change', e => {
const id = e.currentTarget.value;
// Update the selection.
const position = currentSelection.indexOf(id);
if (e.currentTarget.checked) {
// Check if the ID is not already in the selection and add if needed.
if (position === -1) {
currentSelection.push(id);
}
} else if (position !== -1) {
// Remove the ID when it is in the current selection.
currentSelection.splice(position, 1);
}
// Set the selection in the hidden form element.
$form
.find('#media-library-modal-selection')
.val(currentSelection.join())
.trigger('change');
// Set the selection in the media library add form. Since the form is
// not necessarily loaded within the same context, we can't use the
// context here.
$('.js-media-library-add-form-current-selection').val(
currentSelection.join(),
);
});
// The hidden selection form field changes when the selection is updated.
$('#media-library-modal-selection', $form)
.once('media-library-selection-change')
.on('change', e => {
updateSelectionCount(settings.media_library.selection_remaining);
// Prevent users from selecting more items than allowed.
if (
currentSelection.length ===
settings.media_library.selection_remaining
) {
disableItems($mediaItems.not(':checked'));
enableItems($mediaItems.filter(':checked'));
} else {
enableItems($mediaItems);
}
});
// Apply the current selection to the media library view. Changing the
// checkbox values triggers the change event for the media items. The
// change event handles updating the hidden selection field for the form.
currentSelection.forEach(value => {
$form
.find(`input[type="checkbox"][value="${value}"]`)
.prop('checked', true)
.trigger('change');
});
// Add the selection count to the button pane when a media library dialog
// is created.
$(window)
.once('media-library-selection-info')
.on('dialog:aftercreate', () => {
// Since the dialog HTML is not part of the context, we can't use
// context here.
const $buttonPane = $(
'.media-library-widget-modal .ui-dialog-buttonpane',
);
if (!$buttonPane.length) {
return;
}
$buttonPane.append(Drupal.theme('mediaLibrarySelectionCount'));
updateSelectionCount(settings.media_library.selection_remaining);
});
},
};
/**
* Clear the current selection.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to clear the selection when the library modal closes.
*/
Drupal.behaviors.MediaLibraryModalClearSelection = {
attach() {
$(window)
.once('media-library-clear-selection')
.on('dialog:afterclose', () => {
Drupal.MediaLibrary.currentSelection = [];
});
},
};
/**
* Theme function for the selection count.
*
* @return {string}
* The corresponding HTML.
*/
Drupal.theme.mediaLibrarySelectionCount = function() {
return `<div class="media-library-selected-count js-media-library-selected-count" role="status" aria-live="polite" aria-atomic="true"></div>`;
};
})(jQuery, Drupal, window);
@@ -0,0 +1,216 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal, window) {
Drupal.MediaLibrary = {
currentSelection: []
};
Drupal.AjaxCommands.prototype.updateMediaLibrarySelection = function (ajax, response, status) {
Object.values(response.mediaIds).forEach(function (value) {
Drupal.MediaLibrary.currentSelection.push(value);
});
};
Drupal.behaviors.MediaLibraryTabs = {
attach: function attach(context) {
var $menu = $('.js-media-library-menu');
$menu.find('a', context).once('media-library-menu-item').on('keypress', function (e) {
if (e.which === 32) {
e.preventDefault();
e.stopPropagation();
$(e.currentTarget).trigger('click');
}
}).on('click', function (e) {
e.preventDefault();
e.stopPropagation();
var ajaxObject = Drupal.ajax({
wrapper: 'media-library-content',
url: e.currentTarget.href,
dialogType: 'ajax',
progress: {
type: 'fullscreen',
message: Drupal.t('Please wait...')
}
});
ajaxObject.success = function (response, status) {
var _this = this;
if (this.progress.element) {
$(this.progress.element).remove();
}
if (this.progress.object) {
this.progress.object.stopMonitoring();
}
$(this.element).prop('disabled', false);
Object.keys(response || {}).forEach(function (i) {
if (response[i].command && _this.commands[response[i].command]) {
_this.commands[response[i].command](_this, response[i], status);
}
});
$('#media-library-content :tabbable:first').focus();
this.settings = null;
};
ajaxObject.execute();
$menu.find('.active-tab').remove();
$menu.find('a').removeClass('active');
$(e.currentTarget).addClass('active').html(Drupal.t('<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span><span class="active-tab visually-hidden"> (selected)</span>', { '@title': $(e.currentTarget).data('title') }));
Drupal.announce(Drupal.t('Showing @title media.', {
'@title': $(e.currentTarget).data('title')
}));
});
}
};
Drupal.behaviors.MediaLibraryViewsDisplay = {
attach: function attach(context) {
var $view = $(context).hasClass('.js-media-library-view') ? $(context) : $('.js-media-library-view', context);
$view.closest('.views-element-container').attr('id', 'media-library-view');
$('.views-display-link-widget, .views-display-link-widget_table', context).once('media-library-views-display-link').on('click', function (e) {
e.preventDefault();
e.stopPropagation();
var $link = $(e.currentTarget);
var loadingAnnouncement = '';
var displayAnnouncement = '';
var focusSelector = '';
if ($link.hasClass('views-display-link-widget')) {
loadingAnnouncement = Drupal.t('Loading grid view.');
displayAnnouncement = Drupal.t('Changed to grid view.');
focusSelector = '.views-display-link-widget';
} else if ($link.hasClass('views-display-link-widget_table')) {
loadingAnnouncement = Drupal.t('Loading table view.');
displayAnnouncement = Drupal.t('Changed to table view.');
focusSelector = '.views-display-link-widget_table';
}
var ajaxObject = Drupal.ajax({
wrapper: 'media-library-view',
url: e.currentTarget.href,
dialogType: 'ajax',
progress: {
type: 'fullscreen',
message: loadingAnnouncement || Drupal.t('Please wait...')
}
});
if (displayAnnouncement || focusSelector) {
var success = ajaxObject.success;
ajaxObject.success = function (response, status) {
success.bind(this)(response, status);
if (focusSelector) {
$(focusSelector).focus();
}
if (displayAnnouncement) {
Drupal.announce(displayAnnouncement);
}
};
}
ajaxObject.execute();
if (loadingAnnouncement) {
Drupal.announce(loadingAnnouncement);
}
});
}
};
Drupal.behaviors.MediaLibraryItemSelection = {
attach: function attach(context, settings) {
var $form = $('.js-media-library-views-form, .js-media-library-add-form', context);
var currentSelection = Drupal.MediaLibrary.currentSelection;
if (!$form.length) {
return;
}
var $mediaItems = $('.js-media-library-item input[type="checkbox"]', $form);
function disableItems($items) {
$items.prop('disabled', true).closest('.js-media-library-item').addClass('media-library-item--disabled');
}
function enableItems($items) {
$items.prop('disabled', false).closest('.js-media-library-item').removeClass('media-library-item--disabled');
}
function updateSelectionCount(remaining) {
var selectItemsText = remaining < 0 ? Drupal.formatPlural(currentSelection.length, '1 item selected', '@count items selected') : Drupal.formatPlural(remaining, '@selected of @count item selected', '@selected of @count items selected', {
'@selected': currentSelection.length
});
$('.js-media-library-selected-count').html(selectItemsText);
}
$mediaItems.once('media-item-change').on('change', function (e) {
var id = e.currentTarget.value;
var position = currentSelection.indexOf(id);
if (e.currentTarget.checked) {
if (position === -1) {
currentSelection.push(id);
}
} else if (position !== -1) {
currentSelection.splice(position, 1);
}
$form.find('#media-library-modal-selection').val(currentSelection.join()).trigger('change');
$('.js-media-library-add-form-current-selection').val(currentSelection.join());
});
$('#media-library-modal-selection', $form).once('media-library-selection-change').on('change', function (e) {
updateSelectionCount(settings.media_library.selection_remaining);
if (currentSelection.length === settings.media_library.selection_remaining) {
disableItems($mediaItems.not(':checked'));
enableItems($mediaItems.filter(':checked'));
} else {
enableItems($mediaItems);
}
});
currentSelection.forEach(function (value) {
$form.find('input[type="checkbox"][value="' + value + '"]').prop('checked', true).trigger('change');
});
$(window).once('media-library-selection-info').on('dialog:aftercreate', function () {
var $buttonPane = $('.media-library-widget-modal .ui-dialog-buttonpane');
if (!$buttonPane.length) {
return;
}
$buttonPane.append(Drupal.theme('mediaLibrarySelectionCount'));
updateSelectionCount(settings.media_library.selection_remaining);
});
}
};
Drupal.behaviors.MediaLibraryModalClearSelection = {
attach: function attach() {
$(window).once('media-library-clear-selection').on('dialog:afterclose', function () {
Drupal.MediaLibrary.currentSelection = [];
});
}
};
Drupal.theme.mediaLibrarySelectionCount = function () {
return '<div class="media-library-selected-count js-media-library-selected-count" role="status" aria-live="polite" aria-atomic="true"></div>';
};
})(jQuery, Drupal, window);
@@ -0,0 +1,50 @@
/**
* @file media_library.view.es6.js
*/
(($, Drupal) => {
/**
* Adds checkbox to select all items in the library.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to select all media items.
*/
Drupal.behaviors.MediaLibrarySelectAll = {
attach(context) {
const $view = $(
'.js-media-library-view[data-view-display-id="page"]',
context,
).once('media-library-select-all');
if ($view.length && $view.find('.js-media-library-item').length) {
const $checkbox = $(Drupal.theme('checkbox')).on(
'click',
({ currentTarget }) => {
// Toggle all checkboxes.
const $checkboxes = $(currentTarget)
.closest('.js-media-library-view')
.find('.js-media-library-item input[type="checkbox"]');
$checkboxes
.prop('checked', $(currentTarget).prop('checked'))
.trigger('change');
// Announce the selection.
const announcement = $(currentTarget).prop('checked')
? Drupal.t('All @count items selected', {
'@count': $checkboxes.length,
})
: Drupal.t('Zero items selected');
Drupal.announce(announcement);
},
);
const $label = $(
'<label class="media-library-select-all"></label>',
).text(Drupal.t('Select all media'));
$label.prepend($checkbox);
$view
.find('.js-media-library-item')
.first()
.before($label);
}
},
};
})(jQuery, Drupal);
@@ -0,0 +1,30 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal) {
Drupal.behaviors.MediaLibrarySelectAll = {
attach: function attach(context) {
var $view = $('.js-media-library-view[data-view-display-id="page"]', context).once('media-library-select-all');
if ($view.length && $view.find('.js-media-library-item').length) {
var $checkbox = $(Drupal.theme('checkbox')).on('click', function (_ref) {
var currentTarget = _ref.currentTarget;
var $checkboxes = $(currentTarget).closest('.js-media-library-view').find('.js-media-library-item input[type="checkbox"]');
$checkboxes.prop('checked', $(currentTarget).prop('checked')).trigger('change');
var announcement = $(currentTarget).prop('checked') ? Drupal.t('All @count items selected', {
'@count': $checkboxes.length
}) : Drupal.t('Zero items selected');
Drupal.announce(announcement);
});
var $label = $('<label class="media-library-select-all"></label>').text(Drupal.t('Select all media'));
$label.prepend($checkbox);
$view.find('.js-media-library-item').first().before($label);
}
}
};
})(jQuery, Drupal);
@@ -0,0 +1,102 @@
/**
* @file media_library.widget.es6.js
*/
(($, Drupal, Sortable) => {
/**
* Allows users to re-order their selection with drag+drop.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to re-order selected media items.
*/
Drupal.behaviors.MediaLibraryWidgetSortable = {
attach(context) {
// Allow media items to be re-sorted with drag+drop in the widget.
const selection = context.querySelectorAll('.js-media-library-selection');
selection.forEach(widget => {
Sortable.create(widget, {
draggable: '.js-media-library-item',
handle: '.js-media-library-item-preview',
onEnd: () => {
$(widget)
.children()
.each((index, child) => {
$(child)
.find('.js-media-library-item-weight')
.val(index);
});
},
});
});
},
};
/**
* Allows selection order to be set without drag+drop for accessibility.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to toggle the weight field for media items.
*/
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,
)
.closest('.js-media-library-widget')
.find('.js-media-library-item-weight')
.parent()
.toggle();
})
.text(strings.show);
$('.js-media-library-item-weight', context)
.once('media-library-toggle')
.parent()
.hide();
},
};
/**
* Disable the open button when the user is not allowed to add more items.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches behavior to disable the media library open button.
*/
Drupal.behaviors.MediaLibraryWidgetDisableButton = {
attach(context) {
// When the user returns from the modal to the widget, we want to shift
// the focus back to the open button. If the user is not allowed to add
// more items, the button needs to be disabled. Since we can't shift the
// focus to disabled elements, the focus is set back to the open button
// via JavaScript by adding the 'data-disabled-focus' attribute.
$('.js-media-library-open-button[data-disabled-focus="true"]', context)
.once('media-library-disable')
.each(function() {
$(this).focus();
// There is a small delay between the focus set by the browser and the
// focus of screen readers. We need to give screen readers time to
// shift the focus as well before the button is disabled.
setTimeout(() => {
$(this).attr('disabled', 'disabled');
}, 50);
});
},
};
})(jQuery, Drupal, Sortable);
@@ -0,0 +1,53 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function ($, Drupal, Sortable) {
Drupal.behaviors.MediaLibraryWidgetSortable = {
attach: function attach(context) {
var selection = context.querySelectorAll('.js-media-library-selection');
selection.forEach(function (widget) {
Sortable.create(widget, {
draggable: '.js-media-library-item',
handle: '.js-media-library-item-preview',
onEnd: function onEnd() {
$(widget).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).closest('.js-media-library-widget').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.MediaLibraryWidgetDisableButton = {
attach: function attach(context) {
$('.js-media-library-open-button[data-disabled-focus="true"]', context).once('media-library-disable').each(function () {
var _this = this;
$(this).focus();
setTimeout(function () {
$(_this).attr('disabled', 'disabled');
}, 50);
});
}
};
})(jQuery, Drupal, Sortable);
Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

@@ -0,0 +1,76 @@
/**
* @file
* Drupal Media Library plugin.
*/
(function(Drupal, CKEDITOR) {
CKEDITOR.plugins.add('drupalmedialibrary', {
requires: 'drupalmedia',
icons: 'drupalmedialibrary',
hidpi: true,
beforeInit(editor) {
editor.addCommand('drupalmedialibrary', {
allowedContent: {
'drupal-media': {
attributes: {
'!data-entity-type': true,
'!data-entity-uuid': true,
'!data-view-mode': true,
'!data-align': true,
'!data-caption': true,
'!alt': true,
'!title': true,
},
classes: {},
},
},
// This does not use the object format used above, but a
// CKEDITOR.style instance, because requiredContent does not support
// the object format.
// @see https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_filter_contentRule.html
// eslint-disable-next-line new-cap
requiredContent: new CKEDITOR.style({
element: 'drupal-media',
attributes: {
'data-entity-type': '',
'data-entity-uuid': '',
},
}),
modes: { wysiwyg: 1 },
// There is an edge case related to the undo functionality that will
// be resolved in https://www.drupal.org/project/drupal/issues/3073294.
canUndo: true,
// eslint-disable-next-line no-shadow
exec(editor) {
const saveCallback = function(values) {
editor.fire('saveSnapshot');
const mediaElement = editor.document.createElement('drupal-media');
// eslint-disable-next-line prefer-destructuring
const attributes = values.attributes;
Object.keys(attributes).forEach(key => {
mediaElement.setAttribute(key, attributes[key]);
});
editor.insertHtml(mediaElement.getOuterHtml());
editor.fire('saveSnapshot');
};
// @see \Drupal\media_library\MediaLibraryUiBuilder::dialogOptions()
Drupal.ckeditor.openDialog(
editor,
editor.config.DrupalMediaLibrary_url,
{},
saveCallback,
editor.config.DrupalMediaLibrary_dialogOptions,
);
},
});
if (editor.ui.addButton) {
editor.ui.addButton('DrupalMediaLibrary', {
label: Drupal.t('Insert from Media Library'),
command: 'drupalmedialibrary',
});
}
},
});
})(Drupal, CKEDITOR);
@@ -0,0 +1,65 @@
/**
* DO NOT EDIT THIS FILE.
* See the following change record for more information,
* https://www.drupal.org/node/2815083
* @preserve
**/
(function (Drupal, CKEDITOR) {
CKEDITOR.plugins.add('drupalmedialibrary', {
requires: 'drupalmedia',
icons: 'drupalmedialibrary',
hidpi: true,
beforeInit: function beforeInit(editor) {
editor.addCommand('drupalmedialibrary', {
allowedContent: {
'drupal-media': {
attributes: {
'!data-entity-type': true,
'!data-entity-uuid': true,
'!data-view-mode': true,
'!data-align': true,
'!data-caption': true,
'!alt': true,
'!title': true
},
classes: {}
}
},
requiredContent: new CKEDITOR.style({
element: 'drupal-media',
attributes: {
'data-entity-type': '',
'data-entity-uuid': ''
}
}),
modes: { wysiwyg: 1 },
canUndo: true,
exec: function exec(editor) {
var saveCallback = function saveCallback(values) {
editor.fire('saveSnapshot');
var mediaElement = editor.document.createElement('drupal-media');
var attributes = values.attributes;
Object.keys(attributes).forEach(function (key) {
mediaElement.setAttribute(key, attributes[key]);
});
editor.insertHtml(mediaElement.getOuterHtml());
editor.fire('saveSnapshot');
};
Drupal.ckeditor.openDialog(editor, editor.config.DrupalMediaLibrary_url, {}, saveCallback, editor.config.DrupalMediaLibrary_dialogOptions);
}
});
if (editor.ui.addButton) {
editor.ui.addButton('DrupalMediaLibrary', {
label: Drupal.t('Insert from Media Library'),
command: 'drupalmedialibrary'
});
}
}
});
})(Drupal, CKEDITOR);
@@ -0,0 +1,93 @@
<?php
/**
* @file
* Documentation related to Media Library.
*/
/**
* @defgroup media_library_architecture Media Library Architecture
* @{
*
* Media Library is a UI for the core Media module. It provides a visual
* interface for users to manage media in their site, and it allows authors to
* visually select media for use in entity reference and text fields, using a
* modal dialog.
*
* In order to provide a consistent user experience, Media Library is
* intentionally opinionated, with few extension points and no hooks. Most of
* its code is internal and should not be extended or instantiated by external
* code.
*
* @section openers Openers
* Interaction with the modal media library dialog is mediated by "opener"
* services. All openers must implement
* \Drupal\media_library\MediaLibraryOpenerInterface.
*
* Openers are responsible for determining access to the media library, and for
* generating an AJAX response when the user has finished selecting media items
* in the library. An opener is a "bridge" between the opinionated media library
* modal dialog and whatever is consuming it, allowing the dialog to be
* triggered in a way that makes sense for that particular consumer. Examples in
* Drupal core include entity reference fields and text editors.
*
* @see \Drupal\media_library\MediaLibraryOpenerInterface
* @see \Drupal\media_library\MediaLibraryEditorOpener
* @see \Drupal\media_library\MediaLibraryFieldWidgetOpener
*
* @section state Modal dialog state
* When the media library modal is used, its configuration and state (such as
* how many items are currently selected, the maximum number that can be
* selected, which media types the user is allowed to see, and so forth) are
* stored in an instance of \Drupal\media_library\MediaLibraryState. The state
* object also stores the service ID of the opener being used, as well as any
* additional parameters or data that are specific to that opener.
*
* The media library state is passed between the user and the server in the
* URL's query parameters. Therefore, the state is also protected by a hash in
* order to prevent tampering.
*
* @see \Drupal\media_library\MediaLibraryState
*
* @section add_form Adding media in the dialog
* Users with appropriate permissions can add media to the library from directly
* within the modal dialog.
*
* This interaction is implemented using forms, and is customizable by modules.
* Since the media library is segmented by media type, each media type can
* expose a different form for adding media of that type; the type's source
* plugin specifies the actual form class to use. Here is an example of a media
* source plugin definition which provides an add form for the media library:
*
* @code
* @MediaSource(
* id = "file",
* label = @Translation("File"),
* description = @Translation("Use local files for reusable media."),
* allowed_field_types = {"file"},
* default_thumbnail_filename = "generic.png",
* forms = {
* "media_library_add" = "\Drupal\media_library\Form\FileUploadForm",
* },
* )
* @endcode
*
* This can also be done in hook_media_source_info_alter(). For example:
*
* @code
* function example_media_source_info_alter(array &$sources) {
* $sources['file']['forms']['media_library_add'] = "\Drupal\media_library\Form\FileUploadForm";
* }
* @endcode
*
* The add form is a standard form class, and can be altered by modules and
* themes just like any other form. For easier implementation, it is recommended
* that modules extend \Drupal\media_library\Form\AddFormBase when providing add
* forms.
*
* @see \Drupal\media_library\Form\AddFormBase
* @see \Drupal\media_library\Form\FileUploadForm
* @see \Drupal\media_library\Form\OEmbedForm
*
* @}
*/
@@ -0,0 +1,11 @@
name: 'Media Library'
type: module
description: 'Enhances the media list with additional features to more easily find and use existing media items.'
package: Core
version: VERSION
core: 8.x
configure: media_library.settings
dependencies:
- drupal:media
- drupal:views
- drupal:user
@@ -0,0 +1,204 @@
<?php
/**
* @file
* Install, update and uninstall functions for the media_library module.
*/
use Drupal\media\Entity\MediaType;
/**
* Implements hook_install().
*/
function media_library_install($is_syncing) {
if (!$is_syncing) {
foreach (MediaType::loadMultiple() as $type) {
_media_library_configure_form_display($type);
_media_library_configure_view_display($type);
}
}
}
/**
* Create the 'media_library' image style.
*/
function media_library_update_8701() {
// This update function has been moved to
// media_library_post_update_add_media_library_image_style().
}
/**
* Updates the media library view widget display (contextual) filters.
*/
function media_library_update_8702() {
$view = \Drupal::configFactory()->getEditable('views.view.media_library');
if ($view && $view->get('display.widget')) {
$view->set('display.widget.display_options.defaults.filters', FALSE);
$view->set('display.widget.display_options.defaults.filter_groups', FALSE);
$view->set('display.widget.display_options.defaults.arguments', FALSE);
$view->set('display.widget.display_options.filters', [
'status' => [
'id' => 'status',
'table' => 'media_field_data',
'field' => 'status',
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => '',
'operator' => '=',
'value' => '1',
'group' => 1,
'exposed' => FALSE,
'expose' => [
'operator_id' => '',
'label' => '',
'description' => '',
'use_operator' => FALSE,
'operator' => '',
'identifier' => '',
'required' => FALSE,
'remember' => FALSE,
'multiple' => FALSE,
'remember_roles' => [
'authenticated' => 'authenticated',
],
],
'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' => 'status',
'plugin_id' => 'boolean',
],
'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',
],
]);
$view->set('display.widget.display_options.filter_groups', [
'operator' => 'AND',
'groups' => [
1 => 'AND',
],
]);
$view->set('display.widget.display_options.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',
],
]);
$view->save(TRUE);
}
}
/**
* This update has been removed and will not run.
*/
function media_library_update_8703() {
// This update function previously added 'edit' and 'delete' buttons to media
// items in the 'media' view. It has been converted to a post-update hook.
// @see media_library_post_update_add_buttons_to_page_view()
}
/**
* Creates the media_library.settings config object.
*/
function media_library_update_8704() {
\Drupal::configFactory()
->getEditable('media_library.settings')
// Enable the advanced UI by default, to preserve existing behavior.
->set('advanced_ui', TRUE)
->save();
}
@@ -0,0 +1,35 @@
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:
- core/drupal.announce
- core/drupal.checkbox
- media_library/click_to_select
widget:
version: VERSION
js:
js/media_library.widget.js: {}
dependencies:
- core/drupal.dialog.ajax
- core/jquery.once
- core/sortable
ui:
version: VERSION
js:
js/media_library.ui.js: {}
dependencies:
- core/drupal.ajax
- core/drupal.announce
- core/jquery.once
- media_library/view
@@ -0,0 +1,5 @@
media_library.add:
route_name: entity.media.add_page
title: 'Add media'
appears_on:
- view.media_library.page
@@ -0,0 +1,5 @@
media_library.settings:
title: 'Media Library settings'
parent: system.admin_config_media
description: 'Manage Media Library settings.'
route_name: media_library.settings
@@ -0,0 +1,10 @@
media_library.table:
title: 'Table'
parent_id: entity.media.collection
route_name: entity.media.collection
weight: 10
media_library.grid:
title: 'Grid'
parent_id: entity.media.collection
route_name: view.media_library.page
weight: 20
@@ -0,0 +1,539 @@
<?php
/**
* @file
* Contains hook implementations for the media_library module.
*/
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\Entity\EntityViewDisplay;
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\Session\AccountInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\image\Entity\ImageStyle;
use Drupal\image\Plugin\Field\FieldType\ImageItem;
use Drupal\media\MediaTypeForm;
use Drupal\media\MediaTypeInterface;
use Drupal\media_library\Form\FileUploadForm;
use Drupal\media_library\Form\OEmbedForm;
use Drupal\media_library\MediaLibraryState;
use Drupal\views\Plugin\views\cache\CachePluginBase;
use Drupal\views\ViewExecutable;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Component\Serialization\Json;
/**
* Implements hook_help().
*
* @todo Update in https://www.drupal.org/project/drupal/issues/2964789
*/
function media_library_help($route_name, RouteMatchInterface $route_match) {
switch ($route_name) {
case 'help.page.media_library':
$output = '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The Media Library module provides a rich, visual interface for managing media, and allows media to be reused in entity reference fields or embedded into text content. It overrides the <a href=":media-collection">media administration page</a>, allowing users to toggle between the existing table-style interface and a new grid-style interface for browsing and performing administrative operations on media.', [
':media-collection' => Url::fromRoute('entity.media.collection')->toString(),
]) . '</p>';
$output .= '<p>' . t('To learn more about media management, begin by reviewing the <a href=":media-help">documentation for the Media module</a>. For more information about the media library and related functionality, see the <a href=":media-library-handbook">online documentation for the Media Library module</a>.', [
':media-help' => Url::fromRoute('help.page', ['name' => 'media'])->toString(),
':media-library-handbook' => 'https://www.drupal.org/docs/8/core/modules/media-library-module',
]) . '</p>';
$output .= '<h3>' . t('Selection dialog') . '</h3>';
$output .= '<p>' . t('When selecting media for an entity reference field or a text editor, Media Library opens a modal dialog to help users easily find and select media. The modal dialog can toggle between a grid-style and table-style interface, and new media items can be uploaded directly into it.') . '</p>';
$output .= '<p>' . t('Within the dialog, media items are divided up by type. If more than one media type can be selected by the user, the available types will be displayed as a set of vertical tabs. To users who have appropriate permissions, each media type may also present a short form allowing you to upload or create new media items of that type.') . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Grid-style vs. table-style interface') . '</dt>';
$output .= '<dd>' . t('The Media Library module provides a new grid-style interface for the media administration page that displays media as thumbnails, with minimal textual information, allowing users to visually browse media in their site. The existing table-style interface is better suited to displaying additional information about media items, in addition to being more accessible to users with assistive technology.') . '</dd>';
$output .= '<dt>' . t('Reusing media in entity reference fields') . '</dt>';
$output .= '<dd>' . t('Any entity reference field that references media can use the media library. To enable, configure the form display for the field to use the "Media library" widget.') . '</dd>';
$output .= '<dt>' . t('Embedding media in text content') . '</dt>';
$output .= '<dd>' . t('To use the media library within CKEditor, you must add the "Insert from Media Library" button to the CKEditor toolbar, and enable the "Embed media" filter in the text format associated with the text editor.') . '</dd>';
$output .= '</dl>';
$output .= '<h3>' . t('Customize') . '</h3>';
$output .= '<ul>';
$output .= '<li>';
if (\Drupal::moduleHandler()->moduleExists('views_ui') && \Drupal::currentUser()->hasPermission('administer views')) {
$output .= t('Both the table-style and grid-style interfaces are regular views and can be customized via the <a href=":views-ui">Views UI</a>, including sorting and filtering. This is the case for both the administration page and the modal dialog.', [
':views_ui' => Url::fromRoute('entity.view.collection')->toString(),
]);
}
else {
$output .= t('Both the table-style and grid-style interfaces are regular views and can be customized via the Views UI, including sorting and filtering. This is the case for both the administration page and the modal dialog.');
}
$output .= '</li>';
$output .= '<li>' . t('In the grid-style interface, the fields that are displayed (including which image style is used for images) can be customized by configuring the "Media library" view mode for each of your <a href=":media-types">media types</a>. The thumbnail images in the grid-style interface can be customized by configuring the "Media Library thumbnail (220×220)" image style.', [
':media-types' => Url::fromRoute('entity.media_type.collection')->toString(),
]) . '</li>';
$output .= '<li>' . t('When adding new media items within the modal dialog, the fields that are displayed can be customized by configuring the "Media library" form mode for each of your <a href=":media-types">media types</a>.', [
':media-types' => Url::fromRoute('entity.media_type.collection')->toString(),
]) . '</li>';
$output .= '</ul>';
return $output;
}
}
/**
* Implements hook_media_source_info_alter().
*/
function media_library_media_source_info_alter(array &$sources) {
$sources['audio_file']['forms']['media_library_add'] = FileUploadForm::class;
$sources['file']['forms']['media_library_add'] = FileUploadForm::class;
$sources['image']['forms']['media_library_add'] = FileUploadForm::class;
$sources['video_file']['forms']['media_library_add'] = FileUploadForm::class;
$sources['oembed:video']['forms']['media_library_add'] = OEmbedForm::class;
}
/**
* Implements hook_theme().
*/
function media_library_theme() {
return [
'media__media_library' => [
'base hook' => 'media',
],
'media_library_wrapper' => [
'render element' => 'element',
],
'media_library_item' => [
'render element' => 'element',
],
];
}
/**
* Prepares variables for the media library modal dialog.
*
* Default template: media-library-wrapper.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties of the element.
* Properties used: #menu, #content.
*/
function template_preprocess_media_library_wrapper(array &$variables) {
$variables['menu'] = &$variables['element']['menu'];
$variables['content'] = &$variables['element']['content'];
}
/**
* Prepares variables for a selected media item.
*
* Default template: media-library-item.html.twig.
*
* @param array $variables
* An associative array containing:
* - element: An associative array containing the properties and children of
* the element.
*/
function template_preprocess_media_library_item(array &$variables) {
$element = &$variables['element'];
foreach (Element::children($element) as $key) {
$variables['content'][$key] = $element[$key];
}
}
/**
* Implements hook_views_pre_render().
*/
function media_library_views_pre_render(ViewExecutable $view) {
$add_classes = function (&$option, array $classes_to_add) {
$classes = $option ? preg_split('/\s+/', trim($option)) : [];
$classes = array_filter($classes);
$classes = array_merge($classes, $classes_to_add);
$option = implode(' ', array_unique($classes));
};
if ($view->id() === 'media_library') {
if ($view->current_display === 'page') {
$add_classes($view->style_plugin->options['row_class'], ['js-media-library-item', 'js-click-to-select']);
if (array_key_exists('media_bulk_form', $view->field)) {
$add_classes($view->field['media_bulk_form']->options['element_class'], ['js-click-to-select-checkbox']);
}
}
elseif (strpos($view->current_display, 'widget') === 0) {
if (array_key_exists('media_library_select_form', $view->field)) {
$add_classes($view->field['media_library_select_form']->options['element_wrapper_class'], ['js-click-to-select-checkbox']);
}
$add_classes($view->display_handler->options['css_class'], ['js-media-library-view']);
}
$add_classes($view->style_plugin->options['row_class'], ['js-media-library-item', 'js-click-to-select']);
if ($view->display_handler->options['defaults']['css_class']) {
$add_classes($view->displayHandlers->get('default')->options['css_class'], ['js-media-library-view']);
}
else {
$add_classes($view->display_handler->options['css_class'], ['js-media-library-view']);
}
}
}
/**
* 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';
if (strpos($view->current_display, 'widget') === 0) {
try {
$query = MediaLibraryState::fromRequest($view->getRequest())->all();
}
catch (InvalidArgumentException $e) {
// MediaLibraryState::fromRequest() will throw an exception if the view
// is being previewed, since not all required query parameters will be
// present. In a preview, however, this can be omitted since we're
// merely previewing.
// @todo Use the views API for checking for the preview mode when it
// lands. https://www.drupal.org/project/drupal/issues/3060855
if (empty($view->preview) && empty($view->live_preview)) {
throw $e;
}
}
// 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);
}
}
}
}
/**
* 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(),
'metadata_attributes' => new Attribute(),
];
$variables['status'] = $media->isPublished();
}
}
/**
* Implements hook_preprocess_views_view() for the 'media_library' view.
*/
function media_library_preprocess_views_view__media_library(array &$variables) {
$variables['attributes']['data-view-display-id'] = $variables['view']->current_display;
}
/**
* Implements hook_preprocess_views_view_fields().
*/
function media_library_preprocess_views_view_fields(&$variables) {
// Add classes to media rendered entity field so it can be targeted for
// JavaScript mouseover and click events.
if ($variables['view']->id() === 'media_library' && isset($variables['fields']['rendered_entity'])) {
if (isset($variables['fields']['rendered_entity']->wrapper_attributes)) {
$variables['fields']['rendered_entity']->wrapper_attributes->addClass('js-click-to-select-trigger');
}
}
}
/**
* 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/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'])) {
/** @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_form_alter().
*/
function media_library_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
// Add a process callback to ensure that the media library view's exposed
// filters submit button is not moved to the modal dialog's button area.
if ($form_id === 'views_exposed_form' && strpos($form['#id'], 'views-exposed-form-media-library-widget') === 0) {
$form['#after_build'][] = '_media_library_views_form_media_library_after_build';
}
// Configures media_library displays when a type is submitted.
if ($form_state->getFormObject() instanceof MediaTypeForm) {
$form['actions']['submit']['#submit'][] = '_media_library_media_type_form_submit';
}
}
/**
* Form #after_build callback for media_library view's exposed filters form.
*/
function _media_library_views_form_media_library_after_build(array $form, FormStateInterface $form_state) {
// Remove .form-actions from the view's exposed filter actions. This prevents
// the "Apply filters" submit button from being moved into the dialog's
// button area.
// @see \Drupal\Core\Render\Element\Actions::processActions
// @see Drupal.behaviors.dialog.prepareDialogButtons
// @todo Remove this after
// https://www.drupal.org/project/drupal/issues/3089751 is fixed.
if (($key = array_search('form-actions', $form['actions']['#attributes']['class'])) !== FALSE) {
unset($form['actions']['#attributes']['class'][$key]);
}
return $form;
}
/**
* Submit callback for media type form.
*/
function _media_library_media_type_form_submit(array &$form, FormStateInterface $form_state) {
$form_object = $form_state->getFormObject();
if ($form_object->getOperation() === 'add') {
$type = $form_object->getEntity();
$form_display_created = _media_library_configure_form_display($type);
$view_display_created = _media_library_configure_view_display($type);
if ($form_display_created || $view_display_created) {
\Drupal::messenger()->addStatus(t('Media Library form and view displays have been created for the %type media type.', [
'%type' => $type->label(),
]));
}
}
}
/**
* 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.
*/
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]);
}
}
}
/**
* Implements hook_ENTITY_TYPE_access().
*/
function media_library_image_style_access(EntityInterface $entity, $operation, AccountInterface $account) {
// Prevent the fallback 'media_library' image style from being deleted.
// @todo: Lock the image style instead of preventing delete access.
// https://www.drupal.org/project/drupal/issues/2247293
if ($operation === 'delete' && $entity->id() === 'media_library') {
return AccessResult::forbidden();
}
}
/**
* Ensures that the given media type has a media_library form display.
*
* @param \Drupal\media\MediaTypeInterface $type
* The media type to configure.
*
* @return bool
* Whether a form display has been created or not.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function _media_library_configure_form_display(MediaTypeInterface $type) {
$display = EntityFormDisplay::load('media.' . $type->id() . '.media_library');
if ($display) {
return FALSE;
}
$values = [
'targetEntityType' => 'media',
'bundle' => $type->id(),
'mode' => 'media_library',
'status' => TRUE,
];
$display = EntityFormDisplay::create($values);
// Remove all default components.
foreach (array_keys($display->getComponents()) as $name) {
$display->removeComponent($name);
}
// Expose the name field when it is not mapped.
$field_map = $type->getFieldMap();
if (empty($field_map['name'])) {
$display->setComponent('name', [
'type' => 'string_textfield',
'settings' => [
'size' => 60,
],
]);
}
// If the source field is an image field, expose it so that users can set alt
// and title text.
$source_field = $type->getSource()->getSourceFieldDefinition($type);
if ($source_field->isDisplayConfigurable('form') && is_a($source_field->getItemDefinition()->getClass(), ImageItem::class, TRUE)) {
$type->getSource()->prepareFormDisplay($type, $display);
}
return (bool) $display->save();
}
/**
* Ensures that the given media type has a media_library view display.
*
* @param \Drupal\media\MediaTypeInterface $type
* The media type to configure.
*
* @return bool
* Whether a view display has been created or not.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
function _media_library_configure_view_display(MediaTypeInterface $type) {
$display = EntityViewDisplay::load('media.' . $type->id() . '.media_library');
if ($display) {
return FALSE;
}
$values = [
'targetEntityType' => 'media',
'bundle' => $type->id(),
'mode' => 'media_library',
'status' => TRUE,
];
$display = EntityViewDisplay::create($values);
// Remove all default components.
foreach (array_keys($display->getComponents()) as $name) {
$display->removeComponent($name);
}
// @todo: Remove dependency on 'medium' and 'thumbnail' image styles from
// media and media library modules.
// https://www.drupal.org/project/drupal/issues/3030437
$image_style = ImageStyle::load('medium');
// Expose the thumbnail component. If the medium image style doesn't exist,
// use the fallback 'media_library' image style.
$display->setComponent('thumbnail', [
'type' => 'image',
'label' => 'hidden',
'settings' => [
'image_style' => $image_style ? $image_style->id() : 'media_library',
'image_link' => '',
],
]);
return (bool) $display->save();
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function media_library_form_filter_format_edit_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
// Add an additional validate callback so so we can ensure the media_embed
// filter is enabled when the DrupalMediaLibrary button is enabled.
$form['#validate'][] = 'media_library_filter_format_edit_form_validate';
}
/**
* Implements hook_form_FORM_ID_alter().
*/
function media_library_form_filter_format_add_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
// Add an additional validate callback so so we can ensure the media_embed
// filter is enabled when the DrupalMediaLibrary button is enabled.
$form['#validate'][] = 'media_library_filter_format_edit_form_validate';
}
/**
* Validate callback to ensure the DrupalMediaLibrary button can work correctly.
*/
function media_library_filter_format_edit_form_validate($form, FormStateInterface $form_state) {
if ($form_state->getTriggeringElement()['#name'] !== 'op') {
return;
}
// The "DrupalMediaLibrary" button is for the CKEditor text editor.
if ($form_state->getValue(['editor', 'editor']) !== 'ckeditor') {
return;
}
$button_group_path = [
'editor',
'settings',
'toolbar',
'button_groups',
];
if ($button_groups = $form_state->getValue($button_group_path)) {
$buttons = [];
$button_groups = Json::decode($button_groups);
foreach ($button_groups as $button_row) {
foreach ($button_row as $button_group) {
$buttons = array_merge($buttons, array_values($button_group['items']));
}
}
$get_filter_label = function ($filter_plugin_id) use ($form) {
return (string) $form['filters']['order'][$filter_plugin_id]['filter']['#markup'];
};
if (in_array('DrupalMediaLibrary', $buttons, TRUE)) {
$media_embed_enabled = $form_state->getValue([
'filters',
'media_embed',
'status',
]);
if (!$media_embed_enabled) {
$error_message = new TranslatableMarkup('The %media-embed-filter-label filter must be enabled to use the %drupal-media-library-button button.', [
'%media-embed-filter-label' => $get_filter_label('media_embed'),
'%drupal-media-library-button' => new TranslatableMarkup('Insert from Media Library'),
]);
$form_state->setErrorByName('filters', $error_message);
}
}
}
}
@@ -0,0 +1,776 @@
<?php
/**
* @file
* Post update functions for Media Library.
*/
use Drupal\Core\Entity\Entity\EntityFormMode;
use Drupal\Core\Entity\Entity\EntityViewMode;
use Drupal\image\Entity\ImageStyle;
use Drupal\media\Entity\MediaType;
use Drupal\user\RoleInterface;
use Drupal\views\Views;
/**
* Create and configure Media Library form and view displays for media types.
*/
function media_library_post_update_display_modes() {
// Ensure the custom view and form modes are created.
$values = [
'id' => 'media.media_library',
'targetEntityType' => 'media',
'label' => t('Media library'),
'dependencies' => [
'enforced' => [
'module' => [
'media_library',
],
],
'module' => [
'media',
],
],
];
if (!EntityViewMode::load('media.media_library')) {
EntityViewMode::create($values)->save();
}
if (!EntityFormMode::load('media.media_library')) {
EntityFormMode::create($values)->save();
}
// The Media Library needs a special form display and view display to make
// sure the Media Library is displayed properly. These were not automatically
// created for custom media types, so let's make sure this is fixed.
$types = [];
foreach (MediaType::loadMultiple() as $type) {
$form_display_created = _media_library_configure_form_display($type);
$view_display_created = _media_library_configure_view_display($type);
if ($form_display_created || $view_display_created) {
$types[] = $type->label();
}
}
if ($types) {
return t('Media Library form and view displays have been created for the following media types: @types.', [
'@types' => implode(', ', $types),
]);
}
}
/**
* Add a table display to the media library view and link grid/table displays.
*/
function media_library_post_update_table_display() {
$view = Views::getView('media_library');
if (!$view) {
return t('The media_library view could not be updated because it has been deleted. The Media Library module needs this view in order to work properly. Uninstall and reinstall the module so the view will be re-created.');
}
// Override CSS classes to allow targeting grid displays.
$view->setDisplay('default');
$default_display = $view->getDisplay('default');
$style = $default_display->getOption('style');
$style['options']['row_class'] = 'media-library-item media-library-item--grid js-media-library-item js-click-to-select';
$default_display->setOption('style', $style);
// Override CSS classes to allow targeting widget displays.
$view->setDisplay('widget');
$grid_display = $view->getDisplay('widget');
$grid_display->overrideOption('css_class', 'media-library-view js-media-library-view media-library-view--widget');
// Create the new table display.
$table_display = $view->newDisplay('page', 'Widget (table)', 'widget_table');
$table_display->setOption('path', 'admin/content/media-widget-table');
// Override CSS classes to allow targeting widget displays.
$table_display->overrideOption('css_class', 'media-library-view js-media-library-view media-library-view--widget');
// Set table as the display style.
$table_display->overrideOption('style', [
'type' => 'table',
'options' => [
'row_class' => 'media-library-item media-library-item--table js-media-library-item js-click-to-select',
'default_row_class' => TRUE,
],
]);
// Set fields for table display.
$table_display->overrideOption('row', [
'type' => 'fields',
]);
$table_display->overrideOption('fields', [
'media_library_select_form' => [
'id' => 'media_library_select_form',
'label' => '',
'table' => 'media',
'field' => 'media_library_select_form',
'relationship' => 'none',
'entity_type' => 'media',
'plugin_id' => 'media_library_select_form',
'element_wrapper_class' => 'js-click-to-select-checkbox',
'element_class' => '',
],
'thumbnail__target_id' => [
'id' => 'thumbnail__target_id',
'label' => 'Thumbnail',
'table' => 'media_field_data',
'field' => 'thumbnail__target_id',
'relationship' => 'none',
'type' => 'image',
'entity_type' => 'media',
'entity_field' => 'thumbnail',
'plugin_id' => 'field',
'settings' => [
'image_style' => 'media_library',
'image_link' => '',
],
],
'name' => [
'id' => 'name',
'label' => 'Name',
'table' => 'media_field_data',
'field' => 'name',
'relationship' => 'none',
'type' => 'string',
'entity_type' => 'media',
'entity_field' => 'name',
'plugin_id' => 'field',
'settings' => [
'link_to_entity' => FALSE,
],
],
'uid' => [
'id' => 'uid',
'label' => 'Author',
'table' => 'media_field_revision',
'field' => 'uid',
'relationship' => 'none',
'type' => 'entity_reference_label',
'entity_type' => 'media',
'entity_field' => 'uid',
'plugin_id' => 'field',
'settings' => [
'link' => TRUE,
],
],
'changed' => [
'id' => 'changed',
'label' => 'Updated',
'table' => 'media_field_data',
'field' => 'changed',
'relationship' => 'none',
'type' => 'timestamp',
'entity_type' => 'media',
'entity_field' => 'changed',
'plugin_id' => 'field',
'settings' => [
'date_format' => 'short',
'custom_date_format' => '',
'timezone' => '',
],
],
]);
// Override the table display options in the same way as the grid display.
$table_display->overrideOption('access', $grid_display->getOption('access'));
$table_display->overrideOption('filters', $grid_display->getOption('filters'));
$table_display->overrideOption('arguments', $grid_display->getOption('arguments'));
$table_display->overrideOption('rendering_language', $grid_display->getOption('rendering_language'));
// Also override the sorts and pager if the grid display has overrides.
$defaults = $grid_display->getOption('defaults');
if (isset($defaults['sorts']) && !$defaults['sorts']) {
$table_display->overrideOption('sorts', $grid_display->getOption('sorts'));
}
if (isset($defaults['pager']) && !$defaults['pager']) {
$table_display->overrideOption('pager', $grid_display->getOption('pager'));
}
// Add display links to both widget and widget table displays.
$display_links = [
'display_link_grid' => [
'id' => 'display_link_grid',
'table' => 'views',
'field' => 'display_link',
'display_id' => 'widget',
'label' => 'Grid',
'plugin_id' => 'display_link',
'empty' => TRUE,
],
'display_link_table' => [
'id' => 'display_link_table',
'table' => 'views',
'field' => 'display_link',
'display_id' => 'widget_table',
'label' => 'Table',
'plugin_id' => 'display_link',
'empty' => TRUE,
],
];
$grid_display->overrideOption('header', $display_links);
$table_display->overrideOption('header', $display_links);
$view->save();
}
/**
* Create the 'media_library' image style if necessary.
*/
function media_library_post_update_add_media_library_image_style() {
// Bail out early if the image style was already created by
// media_library_update_8701(), or manually by the site owner.
if (ImageStyle::load('media_library')) {
return;
}
$image_style = ImageStyle::create([
'name' => 'media_library',
'label' => 'Media Library (220x220)',
]);
// Add a scale effect.
$image_style->addImageEffect([
'id' => 'image_scale',
'weight' => 0,
'data' => [
'width' => 220,
'height' => 220,
'upscale' => FALSE,
],
]);
$image_style->save();
return t('The %label image style has been created successfully.', ['%label' => 'Media Library (220x220)']);
}
/**
* Add a status extra filter to the media library view default display.
*/
function media_library_post_update_add_status_extra_filter() {
$view = Views::getView('media_library');
if (!$view) {
return t('The media_library view could not be updated because it has been deleted. The Media Library module needs this view in order to work properly. Uninstall and reinstall the module so the view will be re-created.');
}
// Fetch the filters from the default display and add the new 'status_extra'
// filter if it does not yet exist.
$default_display = $view->getDisplay();
$filters = $default_display->getOption('filters');
if (!isset($filters['status_extra'])) {
$filters['status_extra'] = [
'group_info' => [
'widget' => 'select',
'group_items' => [],
'multiple' => FALSE,
'description' => '',
'default_group_multiple' => [],
'default_group' => 'All',
'label' => '',
'identifier' => '',
'optional' => TRUE,
'remember' => FALSE,
],
'group' => 1,
'relationship' => 'none',
'exposed' => FALSE,
'expose' => [
'use_operator' => FALSE,
'remember' => FALSE,
'operator_id' => '',
'multiple' => FALSE,
'description' => '',
'required' => FALSE,
'label' => '',
'operator_limit_selection' => FALSE,
'operator' => '',
'identifier' => '',
'operator_list' => [],
'remember_roles' => [RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID],
],
'entity_type' => 'media',
'value' => '',
'field' => 'status_extra',
'is_grouped' => FALSE,
'admin_label' => '',
'operator' => '=',
'table' => 'media_field_data',
'plugin_id' => 'media_status',
'id' => 'status_extra',
'group_type' => 'group',
];
$default_display->setOption('filters', $filters);
$view->save();
return t("The 'Published status or admin user' filter was added to the %label view.", [
'%label' => $view->storage->label(),
]);
}
}
/**
* Add edit and delete button to media library view page display.
*/
function media_library_post_update_add_buttons_to_page_view() {
$view = Views::getView('media_library');
if (!$view) {
return;
}
$display = &$view->storage->getDisplay('page');
if ($display) {
// Fetch the fields from the page display, if the fields are not yet
// overridden, get the fields from the default display.
if (empty($display['display_options']['fields'])) {
$defaults = $view->storage->getDisplay('default');
$display['display_options']['fields'] = $defaults['display_options']['fields'];
// Override the fields for the page display.
$display['display_options']['defaults']['fields'] = FALSE;
}
$fields = $display['display_options']['fields'];
// Check if the name field already exists and add if it doesn't.
if (!isset($fields['name'])) {
$fields['name'] = [
'id' => 'name',
'table' => 'media_field_data',
'field' => 'name',
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => '',
'label' => '',
'exclude' => TRUE,
'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' => '',
'element_default_classes' => TRUE,
'empty' => '',
'hide_empty' => FALSE,
'empty_zero' => FALSE,
'hide_alter_empty' => TRUE,
'click_sort_column' => 'value',
'type' => 'string',
'settings' => [
'link_to_entity' => FALSE,
],
'group_column' => 'value',
'group_columns' => [],
'group_rows' => TRUE,
'delta_limit' => 0,
'delta_offset' => 0,
'delta_reversed' => FALSE,
'delta_first_last' => FALSE,
'multi_type' => 'separator',
'separator' => ', ',
'field_api_classes' => FALSE,
'entity_type' => 'media',
'entity_field' => 'name',
'plugin_id' => 'field',
];
}
// Check if the edit link field already exists and add if it doesn't.
if (!isset($fields['edit_media'])) {
$fields['edit_media'] = [
'id' => 'edit_media',
'table' => 'media',
'field' => 'edit_media',
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => '',
'label' => '',
'exclude' => FALSE,
'alter' => [
'alter_text' => TRUE,
'text' => 'Edit {{ name }}',
'make_link' => TRUE,
'path' => '',
'absolute' => FALSE,
'external' => FALSE,
'replace_spaces' => FALSE,
'path_case' => 'none',
'trim_whitespace' => FALSE,
'alt' => 'Edit {{ name }}',
'rel' => '',
'link_class' => 'media-library-item__edit',
'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' => '0',
'element_wrapper_class' => '',
'element_default_classes' => FALSE,
'empty' => '',
'hide_empty' => FALSE,
'empty_zero' => FALSE,
'hide_alter_empty' => TRUE,
'text' => 'Edit',
'output_url_as_text' => FALSE,
'absolute' => FALSE,
'entity_type' => 'media',
'plugin_id' => 'entity_link_edit',
];
}
// Check if the delete link field already exists and add if it doesn't.
if (!isset($fields['delete_media'])) {
$fields['delete_media'] = [
'id' => 'delete_media',
'table' => 'media',
'field' => 'delete_media',
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => '',
'label' => '',
'exclude' => FALSE,
'alter' => [
'alter_text' => TRUE,
'text' => 'Delete {{ name }}',
'make_link' => TRUE,
'path' => '',
'absolute' => FALSE,
'external' => FALSE,
'replace_spaces' => FALSE,
'path_case' => 'none',
'trim_whitespace' => FALSE,
'alt' => 'Delete {{ name }}',
'rel' => '',
'link_class' => 'media-library-item__remove',
'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' => '0',
'element_wrapper_class' => '',
'element_default_classes' => FALSE,
'empty' => '',
'hide_empty' => FALSE,
'empty_zero' => FALSE,
'hide_alter_empty' => TRUE,
'text' => 'Delete',
'output_url_as_text' => FALSE,
'absolute' => FALSE,
'entity_type' => 'media',
'plugin_id' => 'entity_link_delete',
];
}
// Move the rendered entity field to the last position for accessibility.
$rendered_entity = $fields['rendered_entity'];
unset($fields['rendered_entity']);
$fields['rendered_entity'] = $rendered_entity;
$display['display_options']['fields'] = $fields;
$view->storage->save(TRUE);
}
}
/**
* Add non js prefixed classes to checkboxes if not present.
*
* Note the inclusion of "update_8001" in the function name. This ensures the
* function is executed after media_library_post_update_table_display(), as
* hook_post_update_NAME() implementations within the same file are executed in
* alphanumeric order.
*/
function media_library_post_update_update_8001_checkbox_classes() {
$view = Views::getView('media_library');
if (!$view) {
return;
}
$display_items = [
[
'display_id' => 'default',
'option' => 'element_class',
'field' => 'media_bulk_form',
],
[
'display_id' => 'page',
'option' => 'element_class',
'field' => 'media_bulk_form',
],
[
'display_id' => 'widget',
'option' => 'element_wrapper_class',
'field' => 'media_library_select_form',
],
[
'display_id' => 'widget_table',
'option' => 'element_wrapper_class',
'field' => 'media_library_select_form',
],
];
foreach ($display_items as $item) {
$display_id = $item['display_id'];
$option = $item['option'];
$field = $item['field'];
$display = &$view->storage->getDisplay($display_id);
// Only proceed if the display, field and option exist.
if (!$display || !isset($display['display_options']['fields'][$field][$option])) {
continue;
}
$classes = $display['display_options']['fields'][$field][$option];
$classes_array = preg_split('/\s+/', $classes);
if (!in_array('media-library-item__click-to-select-checkbox', $classes_array, TRUE)) {
$display['display_options']['fields'][$field][$option] = "$classes media-library-item__click-to-select-checkbox";
$view->save();
}
}
}
/**
* Sets /admin/content/media to the table display of the 'media' view.
*/
function media_library_post_update_default_administrative_list_to_table_display() {
$view = Views::getView('media');
if ($view) {
$display = &$view->storage->getDisplay('media_page_list');
if ($display && $display['display_options']['path'] === 'admin/content/media-table') {
$display['display_options']['path'] = 'admin/content/media';
$view->storage->save();
}
}
$view = Views::getView('media_library');
if (!$view) {
return;
}
$display = &$view->storage->getDisplay('page');
if ($display && $display['display_options']['path'] === 'admin/content/media') {
$display['display_options']['path'] .= '-grid';
// Only delete the menu settings if they have not been changed.
if (isset($display['display_options']['menu']) && $display['display_options']['menu']['type'] === 'tab' && $display['display_options']['menu']['title'] === 'Media') {
unset($display['display_options']['menu']);
}
$view->storage->save();
}
}
/**
* Add langcode filters to media library view displays.
*/
function media_library_post_update_add_langcode_filters() {
$view = Views::getView('media_library');
if (!$view) {
return;
}
// Fetch the filters from the default display and add the new 'langcode'
// filter if it does not yet exist.
$default_display = $view->getDisplay();
$filters = $default_display->getOption('filters');
$added_langcode = FALSE;
if (!isset($filters['langcode'])) {
$filters['langcode'] = [
'id' => 'langcode',
'table' => 'media_field_data',
'field' => 'langcode',
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => '',
'operator' => 'in',
'value' => [],
'group' => 1,
'exposed' => TRUE,
'expose' => [
'use_operator' => FALSE,
'remember' => FALSE,
'operator_id' => 'langcode_op',
'multiple' => FALSE,
'description' => '',
'required' => FALSE,
'reduce' => FALSE,
'label' => 'Language',
'operator_limit_selection' => FALSE,
'operator' => 'langcode_op',
'identifier' => 'langcode',
'operator_list' => [],
'remember_roles' => [
'administrator' => '0',
'authenticated' => 'authenticated',
'anonymous' => '0',
],
],
'is_grouped' => FALSE,
'group_info' => [
'widget' => 'select',
'group_items' => [],
'multiple' => FALSE,
'description' => '',
'default_group_multiple' => [],
'default_group' => 'All',
'label' => '',
'identifier' => '',
'optional' => TRUE,
'remember' => FALSE,
],
'entity_type' => 'media',
'entity_field' => 'langcode',
'plugin_id' => 'language',
];
$default_display->setOption('filters', $filters);
$added_langcode = TRUE;
}
$added_default_langcode_displays = [];
foreach (['widget', 'widget_table'] as $display_id) {
// Check if the display still exists, or else skip it.
if (!$view->displayHandlers->has($display_id)) {
continue;
}
$view->setDisplay($display_id);
$display = $view->getDisplay();
// Fetch the filters from the display and add the 'default_langcode' filter
// if it does not yet exist.
$filters = $display->getOption('filters');
if (!isset($filters['default_langcode'])) {
$filters['default_langcode'] = [
'id' => 'default_langcode',
'table' => 'media_field_data',
'field' => 'default_langcode',
'relationship' => 'none',
'group_type' => 'group',
'admin_label' => '',
'operator' => '=',
'value' => '1',
'group' => 1,
'exposed' => FALSE,
'expose' => [
'use_operator' => FALSE,
'remember' => FALSE,
'operator_id' => '',
'multiple' => FALSE,
'description' => '',
'required' => FALSE,
'label' => '',
'operator_limit_selection' => FALSE,
'operator' => '',
'identifier' => '',
'operator_list' => [],
'remember_roles' => [
RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID,
],
],
'is_grouped' => FALSE,
'group_info' => [
'widget' => 'select',
'group_items' => [],
'multiple' => FALSE,
'description' => '',
'default_group_multiple' => [],
'default_group' => 'All',
'label' => '',
'identifier' => '',
'optional' => TRUE,
'remember' => FALSE,
],
'entity_type' => 'media',
'entity_field' => 'default_langcode',
'plugin_id' => 'boolean',
];
$display->setOption('filters', $filters);
// Change the rendering language of the rows to the interface language.
$display->setOption('rendering_language', '***LANGUAGE_language_interface***');
$added_default_langcode_displays[] = $view->storage->get('display')[$display_id]['display_title'];
}
}
if ($added_langcode && $added_default_langcode_displays) {
$view->save();
return t("The 'Language' filter was added to the default display of the %label view and the 'Default translation' filter was added to the following displays: %displays", [
'%label' => $view->storage->label(),
'%displays' => implode(', ', $added_default_langcode_displays),
]);
}
if ($added_langcode) {
$view->save();
return t("The 'Language' filter was added to the default display of the %label view.", [
'%label' => $view->storage->label(),
'%displays' => implode(', ', $added_default_langcode_displays),
]);
}
if ($added_default_langcode_displays) {
$view->save();
return t("The 'Default translation' filter was added to the following %label view displays: %displays", [
'%label' => $view->storage->label(),
'%displays' => implode(', ', $added_default_langcode_displays),
]);
}
}
@@ -0,0 +1,14 @@
media_library.ui:
path: '/media-library'
defaults:
_controller: 'media_library.ui_builder:buildUi'
requirements:
_custom_access: 'media_library.ui_builder:checkAccess'
media_library.settings:
path: '/admin/config/media/media-library'
defaults:
_form: '\Drupal\media_library\Form\SettingsForm'
_title: 'Media Library settings'
requirements:
_permission: 'administer media'
@@ -0,0 +1,18 @@
services:
media_library.ui_builder:
class: Drupal\media_library\MediaLibraryUiBuilder
arguments: ['@entity_type.manager', '@request_stack', '@views.executable', '@form_builder', '@media_library.opener_resolver']
media_library.route_subscriber:
class: Drupal\media_library\Routing\RouteSubscriber
tags:
- { name: event_subscriber }
media_library.opener_resolver:
class: Drupal\media_library\OpenerResolver
calls:
- [setContainer, ['@service_container']]
media_library.opener.field_widget:
class: Drupal\media_library\MediaLibraryFieldWidgetOpener
arguments: ['@entity_type.manager']
media_library.opener.editor:
class: Drupal\media_library\MediaLibraryEditorOpener
arguments: ['@entity_type.manager']
@@ -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;
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\media_library\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* AJAX command for adding media items to the media library selection.
*
* This command instructs the client to add the given media item IDs to the
* current selection of the media library stored in
* Drupal.MediaLibrary.currentSelection.
*
* This command is implemented by
* Drupal.AjaxCommands.prototype.updateMediaLibrarySelection() defined in
* media_library.ui.js.
*
* @ingroup ajax
*
* @internal
* This is an internal part of Media Library and may be subject to change in
* minor releases. External code should not instantiate or extend this class.
*/
class UpdateSelectionCommand implements CommandInterface {
/**
* An array of media IDs to add to the current selection.
*
* @var int[]
*/
protected $mediaIds;
/**
* Constructs an UpdateSelectionCommand object.
*
* @param int[] $media_ids
* An array of media IDs to add to the current selection.
*/
public function __construct(array $media_ids) {
$this->mediaIds = $media_ids;
}
/**
* {@inheritdoc}
*/
public function render() {
return [
'command' => 'updateMediaLibrarySelection',
'mediaIds' => $this->mediaIds,
];
}
}
@@ -0,0 +1,863 @@
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Url;
use Drupal\media\MediaInterface;
use Drupal\media\MediaTypeInterface;
use Drupal\media_library\Ajax\UpdateSelectionCommand;
use Drupal\media_library\MediaLibraryUiBuilder;
use Drupal\media_library\OpenerResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a base class for creating media items from within the media library.
*/
abstract class AddFormBase extends FormBase implements BaseFormIdInterface, TrustedCallbackInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The media library UI builder.
*
* @var \Drupal\media_library\MediaLibraryUiBuilder
*/
protected $libraryUiBuilder;
/**
* The type of media items being created by this form.
*
* @var \Drupal\media\MediaTypeInterface
*/
protected $mediaType;
/**
* The media view builder.
*
* @var \Drupal\Core\Entity\EntityViewBuilderInterface
*/
protected $viewBuilder;
/**
* The opener resolver.
*
* @var \Drupal\media_library\OpenerResolverInterface
*/
protected $openerResolver;
/**
* Constructs a AddFormBase object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder
* The media library UI builder.
* @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
* The opener resolver.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder, OpenerResolverInterface $opener_resolver = NULL) {
$this->entityTypeManager = $entity_type_manager;
$this->libraryUiBuilder = $library_ui_builder;
$this->viewBuilder = $this->entityTypeManager->getViewBuilder('media');
if (!$opener_resolver) {
@trigger_error('The media_library.opener_resolver service must be passed to AddFormBase::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
$opener_resolver = \Drupal::service('media_library.opener_resolver');
}
$this->openerResolver = $opener_resolver;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('media_library.ui_builder'),
$container->get('media_library.opener_resolver')
);
}
/**
* {@inheritdoc}
*/
public function getBaseFormId() {
return 'media_library_add_form';
}
/**
* Get the media type from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media\MediaTypeInterface
* The media type.
*
* @throws \InvalidArgumentException
* If the selected media type does not exist.
*/
protected function getMediaType(FormStateInterface $form_state) {
if ($this->mediaType) {
return $this->mediaType;
}
$state = $this->getMediaLibraryState($form_state);
$selected_type_id = $state->getSelectedTypeId();
$this->mediaType = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id);
if (!$this->mediaType) {
throw new \InvalidArgumentException("The '$selected_type_id' media type does not exist.");
}
return $this->mediaType;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
// @todo Remove the ID when we can use selectors to replace content via
// AJAX in https://www.drupal.org/project/drupal/issues/2821793.
$form['#prefix'] = '<div id="media-library-add-form-wrapper">';
$form['#suffix'] = '</div>';
// The media library is loaded via AJAX, which means that the form action
// URL defaults to the current URL. However, to add media, we always need to
// submit the form to the media library URL, not whatever the current URL
// may be.
$form['#action'] = Url::fromRoute('media_library.ui', [], [
'query' => $this->getMediaLibraryState($form_state)->all(),
])->toString();
// The form is posted via AJAX. When there are messages set during the
// validation or submission of the form, the messages need to be shown to
// the user.
$form['status_messages'] = [
'#type' => 'status_messages',
];
$form['#attributes']['class'] = [
'js-media-library-add-form',
];
$added_media = $this->getAddedMediaItems($form_state);
if (empty($added_media)) {
$form = $this->buildInputElement($form, $form_state);
}
else {
$form['#attributes']['data-input'] = 'true';
// This deserves to be themeable, but it doesn't need to be its own "real"
// template.
$form['description'] = [
'#type' => 'inline_template',
'#template' => '<p>{{ text }}</p>',
'#context' => [
'text' => $this->formatPlural(count($added_media), 'The media item has been created but has not yet been saved. Fill in any required fields and save to add it to the media library.', 'The media items have been created but have not yet been saved. Fill in any required fields and save to add them to the media library.'),
],
];
$form['media'] = [
'#pre_render' => [
[$this, 'preRenderAddedMedia'],
],
'#attributes' => [
'class' => [
// This needs to be focus-able by an AJAX response.
// @see ::updateFormCallback()
'js-media-library-add-form-added-media',
],
'aria-label' => $this->t('Added media items'),
// Add the tabindex '-1' to allow the focus to be shifted to the added
// media wrapper when items are added. We set focus to the container
// because a media item does not necessarily have required fields and
// we do not want to set focus to the remove button automatically.
// @see ::updateFormCallback()
'tabindex' => '-1',
],
];
foreach ($added_media as $delta => $media) {
$form['media'][$delta] = $this->buildEntityFormElement($media, $form, $form_state, $delta);
}
$form['selection'] = $this->buildCurrentSelectionArea($form, $form_state);
$form['actions'] = $this->buildActions($form, $form_state);
}
// Allow the current selection to be set in a hidden field so the selection
// can be passed between different states of the form. This field is filled
// via JavaScript so the default value should be empty.
// @see Drupal.behaviors.MediaLibraryItemSelection
$form['current_selection'] = [
'#type' => 'hidden',
'#default_value' => '',
'#attributes' => [
'class' => [
'js-media-library-add-form-current-selection',
],
],
];
return $form;
}
/**
* Builds the element for submitting source field value(s).
*
* The input element needs to have a submit handler to create media items from
* the user input and store them in the form state using
* ::processInputValues().
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* The complete form, with the element added.
*
* @see ::processInputValues()
*/
abstract protected function buildInputElement(array $form, FormStateInterface $form_state);
/**
* Builds the sub-form for setting required fields on a new media item.
*
* @param \Drupal\media\MediaInterface $media
* A new, unsaved media item.
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param int $delta
* The delta of the media item.
*
* @return array
* The element containing the required fields sub-form.
*/
protected function buildEntityFormElement(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
// We need to make sure each button has a unique name attribute. The default
// name for button elements is 'op'. If the name is not unique, the
// triggering element is not set correctly and the wrong media item is
// removed.
// @see ::removeButtonSubmit()
$parents = isset($form['#parents']) ? $form['#parents'] : [];
$id_suffix = $parents ? '-' . implode('-', $parents) : '';
$element = [
'#wrapper_attributes' => [
'aria-label' => $media->getName(),
// Add the tabindex '-1' to allow the focus to be shifted to the next
// media item when an item is removed. We set focus to the container
// because a media item does not necessarily have required fields and we
// do not want to set focus to the remove button automatically.
// @see ::updateFormCallback()
'tabindex' => '-1',
// Add a data attribute containing the delta to allow us to easily shift
// the focus to a specific media item.
// @see ::updateFormCallback()
'data-media-library-added-delta' => $delta,
],
'preview' => [
'#type' => 'container',
'#weight' => 10,
],
'fields' => [
'#type' => 'container',
'#weight' => 20,
// The '#parents' are set here because the entity form display needs it
// to build the entity form fields.
'#parents' => ['media', $delta, 'fields'],
],
'remove_button' => [
'#type' => 'submit',
'#value' => $this->t('Remove'),
'#name' => 'media-' . $delta . '-remove-button' . $id_suffix,
'#weight' => 30,
'#attributes' => [
'aria-label' => $this->t('Remove @label', ['@label' => $media->getName()]),
],
'#ajax' => [
'callback' => '::updateFormCallback',
'wrapper' => 'media-library-add-form-wrapper',
'message' => $this->t('Removing @label.', ['@label' => $media->getName()]),
],
'#submit' => ['::removeButtonSubmit'],
// Ensure errors in other media items do not prevent removal.
'#limit_validation_errors' => [],
],
];
// @todo Make the image style configurable in
// https://www.drupal.org/node/2988223
$source = $media->getSource();
$plugin_definition = $source->getPluginDefinition();
if ($thumbnail_uri = $source->getMetadata($media, $plugin_definition['thumbnail_uri_metadata_attribute'])) {
$element['preview']['thumbnail'] = [
'#theme' => 'image_style',
'#style_name' => 'media_library',
'#uri' => $thumbnail_uri,
];
}
$form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
// When the name is not added to the form as an editable field, output
// the name as a fixed element to confirm the right file was uploaded.
if (!$form_display->getComponent('name')) {
$element['fields']['name'] = [
'#type' => 'item',
'#title' => $this->t('Name'),
'#markup' => $media->getName(),
];
}
$form_display->buildForm($media, $element['fields'], $form_state);
// Add source field name so that it can be identified in form alter and
// widget alter hooks.
$element['fields']['#source_field_name'] = $this->getSourceFieldName($media->bundle->entity);
// The revision log field is currently not configurable from the form
// display, so hide it by changing the access.
// @todo Make the revision_log_message field configurable in
// https://www.drupal.org/project/drupal/issues/2696555
if (isset($element['fields']['revision_log_message'])) {
$element['fields']['revision_log_message']['#access'] = FALSE;
}
return $element;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRenderAddedMedia'];
}
/**
* Converts the set of newly added media into an item list for rendering.
*
* @param array $element
* The render element to transform.
*
* @return array
* The transformed render element.
*/
public function preRenderAddedMedia(array $element) {
// Transform the element into an item list for rendering.
$element['#theme'] = 'item_list__media_library_add_form_media_list';
$element['#list_type'] = 'ul';
foreach (Element::children($element) as $delta) {
$element['#items'][$delta] = $element[$delta];
unset($element[$delta]);
}
return $element;
}
/**
* Returns a render array containing the current selection.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* A render array containing the current selection.
*/
protected function buildCurrentSelectionArea(array $form, FormStateInterface $form_state) {
$pre_selected_items = $this->getPreSelectedMediaItems($form_state);
if (!$pre_selected_items || !$this->isAdvancedUi()) {
return [];
}
$selection = [
'#type' => 'details',
'#theme_wrappers' => [
'details__media_library_add_form_selected_media',
],
'#open' => FALSE,
'#title' => $this->t('Additional selected media'),
];
foreach ($pre_selected_items as $media_id => $media) {
$selection[$media_id] = $this->buildSelectedItemElement($media, $form, $form_state);
}
return $selection;
}
/**
* Returns a render array for a single pre-selected media item.
*
* @param \Drupal\media\MediaInterface $media
* The media item.
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* A render array of a pre-selected media item.
*/
protected function buildSelectedItemElement(MediaInterface $media, array $form, FormStateInterface $form_state) {
return [
'#theme' => 'media_library_item__small',
'#attributes' => [
'class' => [
'js-media-library-item',
'js-click-to-select',
],
],
'select' => [
'#type' => 'container',
'#attributes' => [
'class' => [
'js-click-to-select-checkbox',
],
],
'select_checkbox' => [
'#type' => 'checkbox',
'#title' => $this->t('Select @name', ['@name' => $media->label()]),
'#title_display' => 'invisible',
'#return_value' => $media->id(),
// The checkbox's value is never processed by this form. It is present
// for usability and accessibility reasons, and only used by
// JavaScript to track whether or not this media item is selected. The
// hidden 'current_selection' field is used to store the actual IDs of
// selected media items.
'#value' => FALSE,
],
],
'rendered_entity' => $this->viewBuilder->view($media, 'media_library'),
];
}
/**
* Returns an array of supported actions for the form.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* An actions element containing the actions of the form.
*/
protected function buildActions(array $form, FormStateInterface $form_state) {
$actions = [
'#type' => 'actions',
'save_select' => [
'#type' => 'submit',
'#button_type' => 'primary',
'#value' => $this->t('Save'),
'#ajax' => [
'callback' => '::updateLibrary',
'wrapper' => 'media-library-add-form-wrapper',
],
],
];
if ($this->isAdvancedUi()) {
$actions['save_select']['#value'] = $this->t('Save and select');
$actions['save_insert'] = [
'#type' => 'submit',
'#value' => $this->t('Save and insert'),
'#ajax' => [
'callback' => '::updateWidget',
'wrapper' => 'media-library-add-form-wrapper',
],
];
}
return $actions;
}
/**
* Creates media items from source field input values.
*
* @param mixed[] $source_field_values
* The values for source fields of the media items.
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*/
protected function processInputValues(array $source_field_values, array $form, FormStateInterface $form_state) {
$media_type = $this->getMediaType($form_state);
$media_storage = $this->entityTypeManager->getStorage('media');
$source_field_name = $this->getSourceFieldName($media_type);
$media = array_map(function ($source_field_value) use ($media_type, $media_storage, $source_field_name) {
return $this->createMediaFromValue($media_type, $media_storage, $source_field_name, $source_field_value);
}, $source_field_values);
// Re-key the media items before setting them in the form state.
$form_state->set('media', array_values($media));
// Save the selected items in the form state so they are remembered when an
// item is removed.
$media = $this->entityTypeManager->getStorage('media')
->loadMultiple(explode(',', $form_state->getValue('current_selection')));
// Any ID can be passed to the form, so we have to check access.
$form_state->set('current_selection', array_filter($media, function ($media_item) {
return $media_item->access('view');
}));
$form_state->setRebuild();
}
/**
* Creates a new, unsaved media item from a source field value.
*
* @param \Drupal\media\MediaTypeInterface $media_type
* The media type of the media item.
* @param \Drupal\Core\Entity\EntityStorageInterface $media_storage
* The media storage.
* @param string $source_field_name
* The name of the media type's source field.
* @param mixed $source_field_value
* The value for the source field of the media item.
*
* @return \Drupal\media\MediaInterface
* An unsaved media entity.
*/
protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $source_field_value) {
$media = $media_storage->create([
'bundle' => $media_type->id(),
$source_field_name => $source_field_value,
]);
$media->setName($media->getName());
return $media;
}
/**
* Prepares a created media item to be permanently saved.
*
* @param \Drupal\media\MediaInterface $media
* The unsaved media item.
*/
protected function prepareMediaEntityForSave(MediaInterface $media) {
// Intentionally empty by default.
}
/**
* Submit handler for the remove button.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function removeButtonSubmit(array $form, FormStateInterface $form_state) {
// Retrieve the delta of the media item from the parents of the remove
// button.
$triggering_element = $form_state->getTriggeringElement();
$delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
$added_media = $form_state->get('media');
$removed_media = $added_media[$delta];
// Update the list of added media items in the form state.
unset($added_media[$delta]);
// Update the media items in the form state.
$form_state->set('media', $added_media)->setRebuild();
// Show a message to the user to confirm the media is removed.
$this->messenger()->addStatus($this->t('The media item %label has been removed.', ['%label' => $removed_media->label()]));
}
/**
* AJAX callback to update the entire form based on source field input.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse|array
* The form render array or an AJAX response object.
*/
public function updateFormCallback(array &$form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$wrapper_id = $triggering_element['#ajax']['wrapper'];
$added_media = $form_state->get('media');
$response = new AjaxResponse();
// When the source field input contains errors, replace the existing form to
// let the user change the source field input. If the user input is valid,
// the entire modal is replaced with the second step of the form to show the
// form fields for each media item.
if ($form_state::hasAnyErrors()) {
$response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $form));
return $response;
}
// Check if the remove button is clicked.
if (end($triggering_element['#parents']) === 'remove_button') {
// When the list of added media is empty, return to the media library and
// shift focus back to the first tabbable element (which should be the
// source field).
if (empty($added_media)) {
$response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
$response->addCommand(new InvokeCommand('#media-library-add-form-wrapper :tabbable', 'focus'));
}
// When there are still more items, update the form and shift the focus to
// the next media item. If the last list item is removed, shift focus to
// the previous item.
else {
$response->addCommand(new ReplaceCommand("#$wrapper_id", $form));
// Find the delta of the next media item. If there is no item with a
// bigger delta, we automatically use the delta of the previous item and
// shift the focus there.
$removed_delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
$delta_to_focus = 0;
foreach ($added_media as $delta => $media) {
$delta_to_focus = $delta;
if ($delta > $removed_delta) {
break;
}
}
$response->addCommand(new InvokeCommand("[data-media-library-added-delta=$delta_to_focus]", 'focus'));
}
}
// Update the form and shift focus to the added media items.
else {
$response->addCommand(new ReplaceCommand("#$wrapper_id", $form));
$response->addCommand(new InvokeCommand('.js-media-library-add-form-added-media', 'focus'));
}
return $response;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
foreach ($this->getAddedMediaItems($form_state) as $delta => $media) {
$this->validateMediaEntity($media, $form, $form_state, $delta);
}
}
/**
* Validate a created media item.
*
* @param \Drupal\media\MediaInterface $media
* The media item to validate.
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param int $delta
* The delta of the media item.
*/
protected function validateMediaEntity(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
$form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
$form_display->extractFormValues($media, $form['media'][$delta]['fields'], $form_state);
$form_display->validateFormValues($media, $form['media'][$delta]['fields'], $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($this->getAddedMediaItems($form_state) as $delta => $media) {
EntityFormDisplay::collectRenderDisplay($media, 'media_library')
->extractFormValues($media, $form['media'][$delta]['fields'], $form_state);
$this->prepareMediaEntityForSave($media);
$media->save();
}
}
/**
* AJAX callback to send the new media item(s) to the media library.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array|\Drupal\Core\Ajax\AjaxResponse
* The form array if there are validation errors, or an AJAX response to add
* the created items to the current selection.
*/
public function updateLibrary(array &$form, FormStateInterface $form_state) {
if ($form_state::hasAnyErrors()) {
return $form;
}
$media_ids = array_map(function (MediaInterface $media) {
return $media->id();
}, $this->getAddedMediaItems($form_state));
$response = new AjaxResponse();
$response->addCommand(new UpdateSelectionCommand($media_ids));
$media_id_to_focus = array_pop($media_ids);
$response->addCommand(new ReplaceCommand('#media-library-add-form-wrapper', $this->buildMediaLibraryUi($form_state)));
$response->addCommand(new InvokeCommand("#media-library-content [value=$media_id_to_focus]", 'focus'));
return $response;
}
/**
* Build the render array of the media library UI.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array
* The render array for the media library.
*/
protected function buildMediaLibraryUi(FormStateInterface $form_state) {
// Get the render array for the media library. The media library state might
// contain the 'media_library_content' when it has been opened from a
// vertical tab. We need to remove that to make sure the render array
// contains the vertical tabs. Besides that, we also need to force the media
// library to create a new instance of the media add form.
// @see \Drupal\media_library\MediaLibraryUiBuilder::buildMediaTypeAddForm()
$state = $this->getMediaLibraryState($form_state);
$state->remove('media_library_content');
$state->set('_media_library_form_rebuild', TRUE);
return $this->libraryUiBuilder->buildUi($state);
}
/**
* AJAX callback to send the new media item(s) to the calling code.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return array|\Drupal\Core\Ajax\AjaxResponse
* The form array when there are form errors or a AJAX response to select
* the created items in the media library.
*/
public function updateWidget(array &$form, FormStateInterface $form_state) {
if ($form_state::hasAnyErrors()) {
return $form;
}
// The added media items get an ID when they are saved in ::submitForm().
// For that reason the added media items are keyed by delta in the form
// state and we have to do an array map to get each media ID.
$current_media_ids = array_map(function (MediaInterface $media) {
return $media->id();
}, $this->getCurrentMediaItems($form_state));
// Allow the opener service to respond to the selection.
$state = $this->getMediaLibraryState($form_state);
return $this->openerResolver->get($state)
->getSelectionResponse($state, $current_media_ids)
->addCommand(new CloseDialogCommand());
}
/**
* Get the media library state from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media_library\MediaLibraryState
* The media library state.
*
* @throws \InvalidArgumentException
* If the media library state is not present in the form state.
*/
protected function getMediaLibraryState(FormStateInterface $form_state) {
$state = $form_state->get('media_library_state');
if (!$state) {
throw new \InvalidArgumentException('The media library state is not present in the form state.');
}
return $state;
}
/**
* Returns the name of the source field for a media type.
*
* @param \Drupal\media\MediaTypeInterface $media_type
* The media type to get the source field name for.
*
* @return string
* The name of the media type's source field.
*/
protected function getSourceFieldName(MediaTypeInterface $media_type) {
return $media_type->getSource()
->getSourceFieldDefinition($media_type)
->getName();
}
/**
* Get all pre-selected media items from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media\MediaInterface[]
* An array containing the pre-selected media items keyed by ID.
*/
protected function getPreSelectedMediaItems(FormStateInterface $form_state) {
// Get the pre-selected media items from the form state.
// @see ::processInputValues()
return $form_state->get('current_selection') ?: [];
}
/**
* Get all added media items from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media\MediaInterface[]
* An array containing the added media items keyed by delta. The media items
* won't have an ID until they are saved in ::submitForm().
*/
protected function getAddedMediaItems(FormStateInterface $form_state) {
return $form_state->get('media') ?: [];
}
/**
* Get all pre-selected and added media items from the form state.
*
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*
* @return \Drupal\media\MediaInterface[]
* An array containing all pre-selected and added media items with
* renumbered numeric keys.
*/
protected function getCurrentMediaItems(FormStateInterface $form_state) {
$pre_selected_media = $this->getPreSelectedMediaItems($form_state);
$added_media = $this->getAddedMediaItems($form_state);
// Using array_merge will renumber the numeric keys.
return array_merge($pre_selected_media, $added_media);
}
/**
* Determines if the "advanced UI" of the Media Library is enabled.
*
* This exposes additional features that are useful to power users.
*
* @return bool
* TRUE if the advanced UI is enabled, FALSE otherwise.
*
* @see ::buildActions()
* @see ::buildCurrentSelectionArea()
*/
protected function isAdvancedUi() {
return (bool) $this->config('media_library.settings')->get('advanced_ui');
}
}
@@ -0,0 +1,380 @@
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\File\Exception\FileWriteException;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
use Drupal\file\FileUsage\FileUsageInterface;
use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
use Drupal\file\Plugin\Field\FieldType\FileItem;
use Drupal\media\MediaInterface;
use Drupal\media\MediaTypeInterface;
use Drupal\media_library\MediaLibraryUiBuilder;
use Drupal\media_library\OpenerResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Creates a form to create media entities from uploaded files.
*
* @internal
* Form classes are internal.
*/
class FileUploadForm extends AddFormBase {
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* The renderer service.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $renderer;
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The file usage service.
*
* @var \Drupal\file\FileUsage\FileUsageInterface
*/
protected $fileUsage;
/**
* Constructs a new FileUploadForm.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder
* The media library UI builder.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info manager.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer service.
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
* The opener resolver.
* @param \Drupal\file\FileUsage\FileUsageInterface $file_usage
* The file usage service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder, ElementInfoManagerInterface $element_info, RendererInterface $renderer, FileSystemInterface $file_system, OpenerResolverInterface $opener_resolver = NULL, FileUsageInterface $file_usage = NULL) {
parent::__construct($entity_type_manager, $library_ui_builder, $opener_resolver);
$this->elementInfo = $element_info;
$this->renderer = $renderer;
$this->fileSystem = $file_system;
if (!$file_usage) {
@trigger_error('Calling FileUploadForm::__construct() without the `file.usage` service is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Pass the `file.usage` service to the constructor. See https://www.drupal.org/node/3075165', E_USER_DEPRECATED);
$file_usage = \Drupal::service('file.usage');
}
$this->fileUsage = $file_usage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('media_library.ui_builder'),
$container->get('element_info'),
$container->get('renderer'),
$container->get('file_system'),
$container->get('media_library.opener_resolver'),
$container->get('file.usage')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return $this->getBaseFormId() . '_upload';
}
/**
* {@inheritdoc}
*/
protected function getMediaType(FormStateInterface $form_state) {
if ($this->mediaType) {
return $this->mediaType;
}
$media_type = parent::getMediaType($form_state);
// The file upload form only supports media types which use a file field as
// a source field.
$field_definition = $media_type->getSource()->getSourceFieldDefinition($media_type);
if (!is_a($field_definition->getClass(), FileFieldItemList::class, TRUE)) {
throw new \InvalidArgumentException('Can only add media types which use a file field as a source field.');
}
return $media_type;
}
/**
* {@inheritdoc}
*/
protected function buildInputElement(array $form, FormStateInterface $form_state) {
// Create a file item to get the upload validators.
$media_type = $this->getMediaType($form_state);
$item = $this->createFileItem($media_type);
/** @var \Drupal\media_library\MediaLibraryState $state */
$state = $this->getMediaLibraryState($form_state);
if (!$state->hasSlotsAvailable()) {
return $form;
}
$slots = $state->getAvailableSlots();
// Add a container to group the input elements for styling purposes.
$form['container'] = [
'#type' => 'container',
];
$process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []);
$form['container']['upload'] = [
'#type' => 'managed_file',
'#title' => $this->formatPlural($slots, 'Add file', 'Add files'),
// @todo Move validation in https://www.drupal.org/node/2988215
'#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']),
'#upload_validators' => $item->getUploadValidators(),
'#multiple' => $slots > 1 || $slots === FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
'#cardinality' => $slots,
'#remaining_slots' => $slots,
];
$file_upload_help = [
'#theme' => 'file_upload_help',
'#upload_validators' => $form['container']['upload']['#upload_validators'],
'#cardinality' => $slots,
];
// The file upload help needs to be rendered since the description does not
// accept render arrays. The FileWidget::formElement() method adds the file
// upload help in the same way, so any theming improvements made to file
// fields would also be applied to this upload field.
// @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::formElement()
$form['container']['upload']['#description'] = $this->renderer->renderPlain($file_upload_help);
return $form;
}
/**
* Validates the upload element.
*
* @param array $element
* The upload element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The processed upload element.
*/
public function validateUploadElement(array $element, FormStateInterface $form_state) {
if ($form_state::hasAnyErrors()) {
// When an error occurs during uploading files, remove all files so the
// user can re-upload the files.
$element['#value'] = [];
}
$values = $form_state->getValue('upload', []);
if (count($values['fids']) > $element['#cardinality'] && $element['#cardinality'] !== FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
$form_state->setError($element, $this->t('A maximum of @count files can be uploaded.', [
'@count' => $element['#cardinality'],
]));
$form_state->setValue('upload', []);
$element['#value'] = [];
}
return $element;
}
/**
* Processes an upload (managed_file) element.
*
* @param array $element
* The upload element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The processed upload element.
*/
public function processUploadElement(array $element, FormStateInterface $form_state) {
$element['upload_button']['#submit'] = ['::uploadButtonSubmit'];
// Limit the validation errors to make sure
// FormValidator::handleErrorsWithLimitedValidation doesn't remove the
// current selection from the form state.
// @see Drupal\Core\Form\FormValidator::handleErrorsWithLimitedValidation()
$element['upload_button']['#limit_validation_errors'] = [
['upload'],
['current_selection'],
];
$element['upload_button']['#ajax'] = [
'callback' => '::updateFormCallback',
'wrapper' => 'media-library-wrapper',
// Add a fixed URL to post the form since AJAX forms are automatically
// posted to <current> instead of $form['#action'].
// @todo Remove when https://www.drupal.org/project/drupal/issues/2504115
// is fixed.
'url' => Url::fromRoute('media_library.ui'),
'options' => [
'query' => $this->getMediaLibraryState($form_state)->all() + [
FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
],
],
];
return $element;
}
/**
* {@inheritdoc}
*/
protected function buildEntityFormElement(MediaInterface $media, array $form, FormStateInterface $form_state, $delta) {
$element = parent::buildEntityFormElement($media, $form, $form_state, $delta);
$source_field = $this->getSourceFieldName($media->bundle->entity);
if (isset($element['fields'][$source_field])) {
$element['fields'][$source_field]['widget'][0]['#process'][] = [static::class, 'hideExtraSourceFieldComponents'];
}
return $element;
}
/**
* Processes an image or file source field element.
*
* @param array $element
* The entity form source field element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
* @param $form
* The complete form.
*
* @return array
* The processed form element.
*/
public static function hideExtraSourceFieldComponents($element, FormStateInterface $form_state, $form) {
// Remove original button added by ManagedFile::processManagedFile().
if (!empty($element['remove_button'])) {
$element['remove_button']['#access'] = FALSE;
}
// Remove preview added by ImageWidget::process().
if (!empty($element['preview'])) {
$element['preview']['#access'] = FALSE;
}
$element['#title_display'] = 'none';
$element['#description_display'] = 'none';
// Remove the filename display.
foreach ($element['#files'] as $file) {
$element['file_' . $file->id()]['filename']['#access'] = FALSE;
}
return $element;
}
/**
* Submit handler for the upload button, inside the managed_file element.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function uploadButtonSubmit(array $form, FormStateInterface $form_state) {
$files = $this->entityTypeManager
->getStorage('file')
->loadMultiple($form_state->getValue('upload', []));
$this->processInputValues($files, $form, $form_state);
}
/**
* {@inheritdoc}
*/
protected function createMediaFromValue(MediaTypeInterface $media_type, EntityStorageInterface $media_storage, $source_field_name, $file) {
if (!($file instanceof FileInterface)) {
throw new \InvalidArgumentException('Cannot create a media item without a file entity.');
}
// Create a file item to get the upload location.
$item = $this->createFileItem($media_type);
$upload_location = $item->getUploadLocation();
if (!$this->fileSystem->prepareDirectory($upload_location, FileSystemInterface::CREATE_DIRECTORY)) {
throw new FileWriteException("The destination directory '$upload_location' is not writable");
}
$file = file_move($file, $upload_location);
if (!$file) {
throw new \RuntimeException("Unable to move file to '$upload_location'");
}
return parent::createMediaFromValue($media_type, $media_storage, $source_field_name, $file);
}
/**
* Create a file field item.
*
* @param \Drupal\media\MediaTypeInterface $media_type
* The media type of the media item.
*
* @return \Drupal\file\Plugin\Field\FieldType\FileItem
* A created file item.
*/
protected function createFileItem(MediaTypeInterface $media_type) {
$field_definition = $media_type->getSource()->getSourceFieldDefinition($media_type);
$data_definition = FieldItemDataDefinition::create($field_definition);
return new FileItem($data_definition);
}
/**
* {@inheritdoc}
*/
protected function prepareMediaEntityForSave(MediaInterface $media) {
/** @var \Drupal\file\FileInterface $file */
$file = $media->get($this->getSourceFieldName($media->bundle->entity))->entity;
$file->setPermanent();
$file->save();
}
/**
* Submit handler for the remove button.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function removeButtonSubmit(array $form, FormStateInterface $form_state) {
// Retrieve the delta of the media item from the parents of the remove
// button.
$triggering_element = $form_state->getTriggeringElement();
$delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
/** @var \Drupal\media\MediaInterface $removed_media */
$removed_media = $form_state->get(['media', $delta]);
$file = $removed_media->get($this->getSourceFieldName($removed_media->bundle->entity))->entity;
if ($file instanceof FileInterface && empty($this->fileUsage->listUsage($file))) {
$file->delete();
}
parent::removeButtonSubmit($form, $form_state);
}
}
@@ -0,0 +1,178 @@
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\media\OEmbed\ResourceException;
use Drupal\media\OEmbed\ResourceFetcherInterface;
use Drupal\media\OEmbed\UrlResolverInterface;
use Drupal\media\Plugin\media\Source\OEmbedInterface;
use Drupal\media_library\MediaLibraryUiBuilder;
use Drupal\media_library\OpenerResolverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Creates a form to create media entities from oEmbed URLs.
*
* @internal
* Form classes are internal.
*/
class OEmbedForm extends AddFormBase {
/**
* The oEmbed URL resolver service.
*
* @var \Drupal\media\OEmbed\UrlResolverInterface
*/
protected $urlResolver;
/**
* The oEmbed resource fetcher service.
*
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
*/
protected $resourceFetcher;
/**
* Constructs a new OEmbedForm.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\media_library\MediaLibraryUiBuilder $library_ui_builder
* The media library UI builder.
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
* The oEmbed URL resolver service.
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
* The oEmbed resource fetcher service.
* @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
* The opener resolver.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, MediaLibraryUiBuilder $library_ui_builder, UrlResolverInterface $url_resolver, ResourceFetcherInterface $resource_fetcher, OpenerResolverInterface $opener_resolver = NULL) {
parent::__construct($entity_type_manager, $library_ui_builder, $opener_resolver);
$this->urlResolver = $url_resolver;
$this->resourceFetcher = $resource_fetcher;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('media_library.ui_builder'),
$container->get('media.oembed.url_resolver'),
$container->get('media.oembed.resource_fetcher'),
$container->get('media_library.opener_resolver')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return $this->getBaseFormId() . '_oembed';
}
/**
* {@inheritdoc}
*/
protected function getMediaType(FormStateInterface $form_state) {
if ($this->mediaType) {
return $this->mediaType;
}
$media_type = parent::getMediaType($form_state);
if (!$media_type->getSource() instanceof OEmbedInterface) {
throw new \InvalidArgumentException('Can only add media types which use an oEmbed source plugin.');
}
return $media_type;
}
/**
* {@inheritdoc}
*/
protected function buildInputElement(array $form, FormStateInterface $form_state) {
$media_type = $this->getMediaType($form_state);
$providers = $media_type->getSource()->getProviders();
// Add a container to group the input elements for styling purposes.
$form['container'] = [
'#type' => 'container',
];
$form['container']['url'] = [
'#type' => 'url',
'#title' => $this->t('Add @type via URL', [
'@type' => $this->getMediaType($form_state)->label(),
]),
'#description' => $this->t('Allowed providers: @providers.', [
'@providers' => implode(', ', $providers),
]),
'#required' => TRUE,
'#attributes' => [
'placeholder' => 'https://',
],
];
$form['container']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Add'),
'#button_type' => 'primary',
'#validate' => ['::validateUrl'],
'#submit' => ['::addButtonSubmit'],
// @todo Move validation in https://www.drupal.org/node/2988215
'#ajax' => [
'callback' => '::updateFormCallback',
'wrapper' => 'media-library-wrapper',
// Add a fixed URL to post the form since AJAX forms are automatically
// posted to <current> instead of $form['#action'].
// @todo Remove when https://www.drupal.org/project/drupal/issues/2504115
// is fixed.
'url' => Url::fromRoute('media_library.ui'),
'options' => [
'query' => $this->getMediaLibraryState($form_state)->all() + [
FormBuilderInterface::AJAX_FORM_REQUEST => TRUE,
],
],
],
];
return $form;
}
/**
* Validates the oEmbed URL.
*
* @param array $form
* The complete form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current form state.
*/
public function validateUrl(array &$form, FormStateInterface $form_state) {
$url = $form_state->getValue('url');
if ($url) {
try {
$resource_url = $this->urlResolver->getResourceUrl($url);
$this->resourceFetcher->fetchResource($resource_url);
}
catch (ResourceException $e) {
$form_state->setErrorByName('url', $e->getMessage());
}
}
}
/**
* Submit handler for the add button.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function addButtonSubmit(array $form, FormStateInterface $form_state) {
$this->processInputValues([$form_state->getValue('url')], $form, $form_state);
}
}
@@ -0,0 +1,54 @@
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Defines a form for configuring the Media Library module.
*
* @internal
* Form classes are internal.
*/
class SettingsForm extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getEditableConfigNames() {
return ['media_library.settings'];
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'media_library_settings_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['advanced_ui'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enable advanced UI'),
'#default_value' => $this->config('media_library.settings')->get('advanced_ui'),
'#description' => $this->t('If checked, users creating new media items in the media library will see a summary of their selected media items, and they will be able insert their selection directly into the media field or text editor.'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->config('media_library.settings')
->set('advanced_ui', (bool) $form_state->getValue('advanced_ui'))
->save();
parent::submitForm($form, $form_state);
}
}
@@ -0,0 +1,81 @@
<?php
namespace Drupal\media_library;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\editor\Ajax\EditorDialogSave;
/**
* The media library opener for text editors.
*
* @see \Drupal\media_library\Plugin\CKEditorPlugin\DrupalMediaLibrary
*
* @internal
* This service is an internal part of Media Library's CKEditor integration.
*/
class MediaLibraryEditorOpener implements MediaLibraryOpenerInterface {
/**
* The text format entity storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $filterStorage;
/**
* The media storage.
*
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
*/
protected $mediaStorage;
/**
* The MediaLibraryEditorOpener constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->filterStorage = $entity_type_manager->getStorage('filter_format');
$this->mediaStorage = $entity_type_manager->getStorage('media');
}
/**
* {@inheritdoc}
*/
public function checkAccess(MediaLibraryState $state, AccountInterface $account) {
$filter_format_id = $state->getOpenerParameters()['filter_format_id'];
$filter_format = $this->filterStorage->load($filter_format_id);
if (empty($filter_format)) {
return AccessResult::forbidden()
->addCacheTags(['filter_format_list'])
->setReason("The text format '$filter_format_id' could not be loaded.");
}
$filters = $filter_format->filters();
return $filter_format->access('use', $account, TRUE)
->andIf(AccessResult::allowedIf($filters->has('media_embed') && $filters->get('media_embed')->status === TRUE));
}
/**
* {@inheritdoc}
*/
public function getSelectionResponse(MediaLibraryState $state, array $selected_ids) {
$selected_media = $this->mediaStorage->load(reset($selected_ids));
$response = new AjaxResponse();
$values = [
'attributes' => [
'data-entity-type' => 'media',
'data-entity-uuid' => $selected_media->uuid(),
'data-align' => 'center',
],
];
$response->addCommand(new EditorDialogSave($values));
return $response;
}
}
@@ -0,0 +1,132 @@
<?php
namespace Drupal\media_library;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* The media library opener for field widgets.
*
* @internal
* This service is an internal part of Media Library's field widget.
*/
class MediaLibraryFieldWidgetOpener implements MediaLibraryOpenerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* MediaLibraryFieldWidgetOpener constructor.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function checkAccess(MediaLibraryState $state, AccountInterface $account) {
$parameters = $state->getOpenerParameters() + ['entity_id' => NULL];
$process_result = function ($result) {
if ($result instanceof RefinableCacheableDependencyInterface) {
$result->addCacheContexts(['url.query_args']);
}
return $result;
};
// Forbid access if any of the required parameters are missing.
foreach (['entity_type_id', 'bundle', 'field_name'] as $key) {
if (empty($parameters[$key])) {
return $process_result(AccessResult::forbidden("$key parameter is missing."));
}
}
$entity_type_id = $parameters['entity_type_id'];
$bundle = $parameters['bundle'];
$field_name = $parameters['field_name'];
// Since we defer to a field to determine access, ensure we are dealing with
// a fieldable entity type.
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if (!$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
throw new \LogicException("The media library can only be opened by fieldable entities.");
}
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$access_handler = $this->entityTypeManager->getAccessControlHandler($entity_type_id);
if ($parameters['entity_id']) {
$entity = $storage->load($parameters['entity_id']);
$entity_access = $access_handler->access($entity, 'update', $account, TRUE);
}
else {
$entity_access = $access_handler->createAccess($bundle, $account, [], TRUE);
}
// If entity-level access is denied, there's no point in continuing.
if (!$entity_access->isAllowed()) {
return $process_result($entity_access);
}
// If the entity has not been loaded, create it in memory now.
if (!isset($entity)) {
$values = [];
if ($bundle_key = $entity_type->getKey('bundle')) {
$values[$bundle_key] = $bundle;
}
/** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
$entity = $storage->create($values);
}
$items = $entity->get($field_name);
$field_definition = $items->getFieldDefinition();
if ($field_definition->getType() !== 'entity_reference') {
throw new \LogicException('Expected the media library to be opened by an entity reference field.');
}
if ($field_definition->getFieldStorageDefinition()->getSetting('target_type') !== 'media') {
throw new \LogicException('Expected the media library to be opened by an entity reference field that target media items.');
}
$field_access = $access_handler->fieldAccess('edit', $field_definition, $account, $items, TRUE);
return $process_result($entity_access->andIf($field_access));
}
/**
* {@inheritdoc}
*/
public function getSelectionResponse(MediaLibraryState $state, array $selected_ids) {
$response = new AjaxResponse();
$parameters = $state->getOpenerParameters();
if (empty($parameters['field_widget_id'])) {
throw new \InvalidArgumentException('field_widget_id parameter is missing.');
}
// Create a comma-separated list of media IDs, insert them in the hidden
// field of the widget, and trigger the field update via the hidden submit
// button.
$widget_id = $parameters['field_widget_id'];
$ids = implode(',', $selected_ids);
$response
->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [$ids]))
->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown']));
return $response;
}
}
@@ -0,0 +1,53 @@
<?php
namespace Drupal\media_library;
use Drupal\Core\Session\AccountInterface;
/**
* Defines an interface for media library openers.
*
* Media library opener services allow modules to check access to the media
* library selection dialog and respond to selections. Example use cases that
* require different handling:
* - when used in an entity reference field widget;
* - when used in a text editor.
*
* Openers that require additional parameters or metadata should retrieve them
* from the MediaLibraryState object.
*
* @see \Drupal\media_library\MediaLibraryState
* @see \Drupal\media_library\MediaLibraryState::getOpenerParameters()
*/
interface MediaLibraryOpenerInterface {
/**
* Checks media library access.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The media library.
* @param \Drupal\Core\Session\AccountInterface $account
* The user for which to check access.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*
* @see https://www.drupal.org/project/drupal/issues/3038254
*/
public function checkAccess(MediaLibraryState $state, AccountInterface $account);
/**
* Generates a response after selecting media items in the media library.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The state the media library was in at the time of selection, allowing the
* response to be customized based on that state.
* @param int[] $selected_ids
* The IDs of the selected media items.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The response to update the page after selecting media.
*/
public function getSelectionResponse(MediaLibraryState $state, array $selected_ids);
}
@@ -0,0 +1,272 @@
<?php
namespace Drupal\media_library;
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* A value object for the media library state.
*
* When the media library is opened it needs several parameters to work
* properly. These parameters are normally extracted from the current URL, then
* retrieved from and managed by the MediaLibraryState value object. The
* following parameters are required in order to open the media library:
* - media_library_opener_id: The ID of a container service which implements
* \Drupal\media_library\MediaLibraryOpenerInterface and is responsible for
* interacting with the media library on behalf of the "thing" (e.g., a field
* widget or text editor button) which opened it.
* - media_library_allowed_types: The media types available in the library can
* be restricted to a list of allowed types. This should be an array of media
* type IDs.
* - media_library_selected_type: The media library contains tabs to navigate
* between the different media types. The selected type contains the ID of the
* media type whose tab that should be opened.
* - media_library_remaining: When the opener wants to limit the amount of media
* items that can be selected, it can pass the number of remaining slots. When
* the number of remaining slots is a negative number, an unlimited amount of
* items can be selected.
*
* This object can also carry an optional opener-specific array of arbitrary
* values, under the media_library_opener_parameters key. These values are
* included in the hash generated by ::getHash(), so the end user cannot tamper
* with them either.
*
* @see \Drupal\media_library\MediaLibraryOpenerInterface
*/
class MediaLibraryState extends ParameterBag {
/**
* {@inheritdoc}
*/
public function __construct(array $parameters = []) {
$this->validateRequiredParameters($parameters['media_library_opener_id'], $parameters['media_library_allowed_types'], $parameters['media_library_selected_type'], $parameters['media_library_remaining']);
$parameters += [
'media_library_opener_parameters' => [],
];
parent::__construct($parameters);
$this->set('hash', $this->getHash());
}
/**
* Creates a new MediaLibraryState object.
*
* @param string $opener_id
* The opener ID.
* @param string[] $allowed_media_type_ids
* The allowed media type IDs.
* @param string $selected_type_id
* The selected media type ID.
* @param int $remaining_slots
* The number of remaining items the user is allowed to select or add in the
* library.
* @param array $opener_parameters
* (optional) Any additional opener-specific parameter values.
*
* @return static
* A state object.
*/
public static function create($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots, array $opener_parameters = []) {
$state = new static([
'media_library_opener_id' => $opener_id,
'media_library_allowed_types' => $allowed_media_type_ids,
'media_library_selected_type' => $selected_type_id,
'media_library_remaining' => $remaining_slots,
'media_library_opener_parameters' => $opener_parameters,
]);
return $state;
}
/**
* Get the media library state from a request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return static
* A state object.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown when the hash query parameter is invalid.
*/
public static function fromRequest(Request $request) {
$query = $request->query;
// Create a MediaLibraryState object through the create method to make sure
// all validation runs.
$state = static::create(
$query->get('media_library_opener_id'),
$query->get('media_library_allowed_types', []),
$query->get('media_library_selected_type'),
$query->get('media_library_remaining'),
$query->get('media_library_opener_parameters', [])
);
// The request parameters need to contain a valid hash to prevent a
// malicious user modifying the query string to attempt to access
// inaccessible information.
if (!$state->isValidHash($query->get('hash'))) {
throw new BadRequestHttpException("Invalid media library parameters specified.");
}
// Once we have validated the required parameters, we restore the parameters
// from the request since there might be additional values.
$state->replace($query->all());
return $state;
}
/**
* Validates the required parameters for a new MediaLibraryState object.
*
* @param string $opener_id
* The media library opener service ID.
* @param string[] $allowed_media_type_ids
* The allowed media type IDs.
* @param string $selected_type_id
* The selected media type ID.
* @param int $remaining_slots
* The number of remaining items the user is allowed to select or add in the
* library.
*
* @throws \InvalidArgumentException
* If one of the passed arguments is missing or does not pass the
* validation.
*/
protected function validateRequiredParameters($opener_id, array $allowed_media_type_ids, $selected_type_id, $remaining_slots) {
// The opener ID must be a non-empty string.
if (!is_string($opener_id) || empty(trim($opener_id))) {
throw new \InvalidArgumentException('The opener ID parameter is required and must be a string.');
}
// The allowed media type IDs must be an array of non-empty strings.
if (empty($allowed_media_type_ids) || !is_array($allowed_media_type_ids)) {
throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
}
foreach ($allowed_media_type_ids as $allowed_media_type_id) {
if (!is_string($allowed_media_type_id) || empty(trim($allowed_media_type_id))) {
throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
}
}
// The selected type ID must be a non-empty string.
if (!is_string($selected_type_id) || empty(trim($selected_type_id))) {
throw new \InvalidArgumentException('The selected type parameter is required and must be a string.');
}
// The selected type ID must be present in the list of allowed types.
if (!in_array($selected_type_id, $allowed_media_type_ids, TRUE)) {
throw new \InvalidArgumentException('The selected type parameter must be present in the list of allowed types.');
}
// The remaining slots must be numeric.
if (!is_numeric($remaining_slots)) {
throw new \InvalidArgumentException('The remaining slots parameter is required and must be numeric.');
}
}
/**
* Get the hash for the state object.
*
* @return string
* The hashed parameters.
*/
public function getHash() {
// Create a hash from the required state parameters and the serialized
// optional opener-specific parameters. Sort the allowed types and
// opener parameters so that differences in order do not result in
// different hashes.
$allowed_media_type_ids = array_values($this->getAllowedTypeIds());
sort($allowed_media_type_ids);
$opener_parameters = $this->getOpenerParameters();
ksort($opener_parameters);
$hash = implode(':', [
$this->getOpenerId(),
implode(':', $allowed_media_type_ids),
$this->getSelectedTypeId(),
$this->getAvailableSlots(),
serialize($opener_parameters),
]);
return Crypt::hmacBase64($hash, \Drupal::service('private_key')->get() . Settings::getHashSalt());
}
/**
* Validate a hash for the state object.
*
* @param string $hash
* The hash to validate.
*
* @return string
* The hashed parameters.
*/
public function isValidHash($hash) {
return hash_equals($this->getHash(), $hash);
}
/**
* Returns the ID of the media library opener service.
*
* @return string
* The media library opener service ID.
*/
public function getOpenerId() {
return $this->get('media_library_opener_id');
}
/**
* Returns the media type IDs which can be selected.
*
* @return string[]
* The media type IDs.
*/
public function getAllowedTypeIds() {
return $this->get('media_library_allowed_types');
}
/**
* Returns the selected media type.
*
* @return string
* The selected media type.
*/
public function getSelectedTypeId() {
return $this->get('media_library_selected_type');
}
/**
* Determines if additional media items can be selected.
*
* @return bool
* TRUE if additional items can be selected, otherwise FALSE.
*/
public function hasSlotsAvailable() {
return $this->getAvailableSlots() !== 0;
}
/**
* Returns the number of additional media items that can be selected.
*
* When the value is not available in the URL the default is 0. When a
* negative integer is passed, an unlimited amount of media items can be
* selected.
*
* @return int
* The number of additional media items that can be selected.
*/
public function getAvailableSlots() {
return $this->getInt('media_library_remaining');
}
/**
* Returns all opener-specific parameter values.
*
* @return array
* An associative array of all opener-specific parameter values.
*/
public function getOpenerParameters() {
return $this->get('media_library_opener_parameters', []);
}
}
@@ -0,0 +1,353 @@
<?php
namespace Drupal\media_library;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormState;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\views\ViewExecutableFactory;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Service which builds the media library.
*
* @internal
* This service is an internal part of the modal media library dialog and
* does not provide any extension points.
*/
class MediaLibraryUiBuilder {
use StringTranslationTrait;
/**
* The form builder.
*
* @var \Drupal\Core\Form\FormBuilderInterface
*/
protected $formBuilder;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The currently active request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
protected $request;
/**
* The views executable factory.
*
* @var \Drupal\views\ViewExecutableFactory
*/
protected $viewsExecutableFactory;
/**
* The media library opener resolver.
*
* @var \Drupal\media_library\OpenerResolverInterface
*/
protected $openerResolver;
/**
* Constructs a MediaLibraryUiBuilder instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\views\ViewExecutableFactory $views_executable_factory
* The views executable factory.
* @param \Drupal\Core\Form\FormBuilderInterface $form_builder
* The currently active request object.
* @param \Drupal\media_library\OpenerResolverInterface $opener_resolver
* The opener resolver.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, ViewExecutableFactory $views_executable_factory, FormBuilderInterface $form_builder, OpenerResolverInterface $opener_resolver = NULL) {
$this->entityTypeManager = $entity_type_manager;
$this->request = $request_stack->getCurrentRequest();
$this->viewsExecutableFactory = $views_executable_factory;
$this->formBuilder = $form_builder;
if (!$opener_resolver) {
@trigger_error('The media_library.opener_resolver service must be passed to ' . __METHOD__ . ' and will be required before Drupal 9.0.0.', E_USER_DEPRECATED);
$opener_resolver = \Drupal::service('media_library.opener_resolver');
}
$this->openerResolver = $opener_resolver;
}
/**
* Get media library dialog options.
*
* @return array
* The media library dialog options.
*/
public static function dialogOptions() {
return [
'dialogClass' => 'media-library-widget-modal',
'title' => t('Add or select media'),
'height' => '75%',
'width' => '75%',
];
}
/**
* Build the media library UI.
*
* @param \Drupal\media_library\MediaLibraryState $state
* (optional) The current state of the media library, derived from the
* current request.
*
* @return array
* The render array for the media library.
*/
public function buildUi(MediaLibraryState $state = NULL) {
if (!$state) {
$state = MediaLibraryState::fromRequest($this->request);
}
// When navigating to a media type through the vertical tabs, we only want
// to load the changed library content. This is not only more efficient, but
// also provides a more accessible user experience for screen readers.
if ($state->get('media_library_content') === '1') {
return $this->buildLibraryContent($state);
}
else {
return [
'#theme' => 'media_library_wrapper',
'#attributes' => [
'id' => 'media-library-wrapper',
],
'menu' => $this->buildMediaTypeMenu($state),
'content' => $this->buildLibraryContent($state),
// Attach the JavaScript for the media library UI. The number of
// available slots needs to be added to make sure users can't select
// more items than allowed.
'#attached' => [
'library' => ['media_library/ui'],
'drupalSettings' => [
'media_library' => [
'selection_remaining' => $state->getAvailableSlots(),
],
],
],
];
}
}
/**
* Build the media library content area.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The current state of the media library, derived from the current request.
*
* @return array
* The render array for the media library.
*/
protected function buildLibraryContent(MediaLibraryState $state) {
return [
'#type' => 'container',
'#theme_wrappers' => [
'container__media_library_content',
],
'#attributes' => [
'id' => 'media-library-content',
],
'form' => $this->buildMediaTypeAddForm($state),
'view' => $this->buildMediaLibraryView($state),
];
}
/**
* Check access to the media library.
*
* @param \Drupal\Core\Session\AccountInterface $account
* Run access checks for this account.
* @param \Drupal\media_library\MediaLibraryState $state
* (optional) The current state of the media library, derived from the
* current request.
*
* @return \Drupal\Core\Access\AccessResult
* The access result.
*/
public function checkAccess(AccountInterface $account, MediaLibraryState $state = NULL) {
if (!$state) {
try {
$state = MediaLibraryState::fromRequest($this->request);
}
catch (BadRequestHttpException $e) {
return AccessResult::forbidden($e->getMessage());
}
catch (\InvalidArgumentException $e) {
return AccessResult::forbidden($e->getMessage());
}
}
// Deny access if the view or display are removed.
$view = $this->entityTypeManager->getStorage('view')->load('media_library');
if (!$view) {
return AccessResult::forbidden('The media library view does not exist.')
->setCacheMaxAge(0);
}
if (!$view->getDisplay('widget')) {
return AccessResult::forbidden('The media library widget display does not exist.')
->addCacheableDependency($view);
}
// The user must at least be able to view media in order to access the media
// library.
$can_view_media = AccessResult::allowedIfHasPermission($account, 'view media')
->addCacheableDependency($view);
// Delegate any further access checking to the opener service nominated by
// the media library state.
return $this->openerResolver->get($state)->checkAccess($state, $account)
->andIf($can_view_media);
}
/**
* Get the media type menu for the media library.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The current state of the media library, derived from the current request.
*
* @return array
* The render array for the media type menu.
*/
protected function buildMediaTypeMenu(MediaLibraryState $state) {
// Add the menu for each type if we have more than 1 media type enabled for
// the field.
$allowed_type_ids = $state->getAllowedTypeIds();
if (count($allowed_type_ids) <= 1) {
return [];
}
// @todo: Add a class to the li element.
// https://www.drupal.org/project/drupal/issues/3029227
$menu = [
'#theme' => 'links__media_library_menu',
'#links' => [],
'#attributes' => [
'class' => ['js-media-library-menu'],
],
];
$allowed_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($allowed_type_ids);
$selected_type_id = $state->getSelectedTypeId();
foreach ($allowed_types as $allowed_type_id => $allowed_type) {
$link_state = MediaLibraryState::create($state->getOpenerId(), $state->getAllowedTypeIds(), $allowed_type_id, $state->getAvailableSlots(), $state->getOpenerParameters());
// Add the 'media_library_content' parameter so the response will contain
// only the updated content for the tab.
// @see self::buildUi()
$link_state->set('media_library_content', 1);
$title = $allowed_type->label();
$display_title = [
'#markup' => $this->t('<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span>', ['@title' => $title]),
];
if ($allowed_type_id === $selected_type_id) {
$display_title = [
'#markup' => $this->t('<span class="visually-hidden">Show </span>@title<span class="visually-hidden"> media</span><span class="active-tab visually-hidden"> (selected)</span>', ['@title' => $title]),
];
}
$menu['#links']['media-library-menu-' . $allowed_type_id] = [
'title' => $display_title,
'url' => Url::fromRoute('media_library.ui', [], [
'query' => $link_state->all(),
]),
'attributes' => [
'role' => 'button',
'data-title' => $title,
],
];
}
// Set the active menu item.
$menu['#links']['media-library-menu-' . $selected_type_id]['attributes']['class'][] = 'active';
return $menu;
}
/**
* Get the add form for the selected media type.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The current state of the media library, derived from the current request.
*
* @return array
* The render array for the media type add form.
*/
protected function buildMediaTypeAddForm(MediaLibraryState $state) {
$selected_type_id = $state->getSelectedTypeId();
if (!$this->entityTypeManager->getAccessControlHandler('media')->createAccess($selected_type_id)) {
return [];
}
$selected_type = $this->entityTypeManager->getStorage('media_type')->load($selected_type_id);
$plugin_definition = $selected_type->getSource()->getPluginDefinition();
if (empty($plugin_definition['forms']['media_library_add'])) {
return [];
}
// After the form to add new media is submitted, we need to rebuild the
// media library with a new instance of the media add form. The form API
// allows us to do that by forcing empty user input.
// @see \Drupal\Core\Form\FormBuilder::doBuildForm()
$form_state = new FormState();
if ($state->get('_media_library_form_rebuild')) {
$form_state->setUserInput([]);
$state->remove('_media_library_form_rebuild');
}
$form_state->set('media_library_state', $state);
return $this->formBuilder->buildForm($plugin_definition['forms']['media_library_add'], $form_state);
}
/**
* Get the media library view.
*
* @param \Drupal\media_library\MediaLibraryState $state
* The current state of the media library, derived from the current request.
*
* @return array
* The render array for the media library view.
*/
protected function buildMediaLibraryView(MediaLibraryState $state) {
// @todo Make the view configurable in
// https://www.drupal.org/project/drupal/issues/2971209
$view = $this->entityTypeManager->getStorage('view')->load('media_library');
$view_executable = $this->viewsExecutableFactory->get($view);
$display_id = $state->get('views_display_id', 'widget');
// Make sure the state parameters are set in the request so the view can
// pass the parameters along in the pager, filters etc.
$view_request = $view_executable->getRequest();
$view_request->query->add($state->all());
$view_executable->setRequest($view_request);
$args = [$state->getSelectedTypeId()];
// Make sure the state parameters are set in the request so the view can
// pass the parameters along in the pager, filters etc.
$request = $view_executable->getRequest();
$request->query->add($state->all());
$view_executable->setRequest($request);
$view_executable->setDisplay($display_id);
$view_executable->preExecute($args);
$view_executable->execute($display_id);
return $view_executable->buildRenderable($display_id, $args, FALSE);
}
}
@@ -0,0 +1,36 @@
<?php
namespace Drupal\media_library;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
/**
* Defines a class to get media library openers from the container.
*
* This is intended to be a very thin interface-verifying wrapper around
* services which implement \Drupal\media_library\MediaLibraryOpenerInterface.
* It is not an API and should not be extended or used by code that does not
* interact with the Media Library module.
*
* @internal
* This service is an internal part of the modal media library dialog and
* does not provide any extension points or public API.
*/
class OpenerResolver implements OpenerResolverInterface {
use ContainerAwareTrait;
/**
* {@inheritdoc}
*/
public function get(MediaLibraryState $state) {
$service_id = $state->getOpenerId();
$service = $this->container->get($service_id);
if ($service instanceof MediaLibraryOpenerInterface) {
return $service;
}
throw new \RuntimeException("$service_id must be an instance of " . MediaLibraryOpenerInterface::class);
}
}
@@ -0,0 +1,35 @@
<?php
namespace Drupal\media_library;
/**
* Defines an interface to get a media library opener from the container.
*
* This is intended to be a very thin interface-verifying wrapper around
* services which implement \Drupal\media_library\MediaLibraryOpenerInterface.
* It is not an API and should not be extended or used by code that does not
* interact with the Media Library module.
*
* @internal
* This interface is an internal part of the modal media library dialog and
* is only implemented by \Drupal\media_library\OpenerResolver. It is not a
* public API.
*/
interface OpenerResolverInterface {
/**
* Gets a media library opener service from the container.
*
* @param \Drupal\media_library\MediaLibraryState $state
* A value object representing the state of the media library.
*
* @return \Drupal\media_library\MediaLibraryOpenerInterface
* The media library opener service.
*
* @throws \RuntimeException
* If the requested opener service does not implement
* \Drupal\media_library\MediaLibraryOpenerInterface.
*/
public function get(MediaLibraryState $state);
}
@@ -0,0 +1,170 @@
<?php
namespace Drupal\media_library\Plugin\CKEditorPlugin;
use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleExtensionList;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Url;
use Drupal\editor\Entity\Editor;
use Drupal\media_library\MediaLibraryState;
use Drupal\media_library\MediaLibraryUiBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines the "drupalmedialibrary" plugin.
*
* @CKEditorPlugin(
* id = "drupalmedialibrary",
* label = @Translation("Embed media from the Media Library"),
* )
*
* @internal
* Plugin classes are internal.
*/
class DrupalMediaLibrary extends CKEditorPluginBase implements ContainerFactoryPluginInterface {
/**
* The module extension list.
*
* @var \Drupal\Core\Extension\ModuleExtensionList
*/
protected $moduleExtensionList;
/**
* The media type entity storage.
*
* @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface
*/
protected $mediaTypeStorage;
/**
* Constructs a new DrupalMediaLibrary plugin object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param array $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
* The module extension list.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(array $configuration, $plugin_id, array $plugin_definition, ModuleExtensionList $extension_list_module, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->moduleExtensionList = $extension_list_module;
$this->mediaTypeStorage = $entity_type_manager->getStorage('media_type');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('extension.list.module'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function isInternal() {
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getDependencies(Editor $editor) {
return [
'drupalmedia',
];
}
/**
* {@inheritdoc}
*/
public function getLibraries(Editor $editor) {
return [
'editor/drupal.editor.dialog',
];
}
/**
* {@inheritdoc}
*/
public function getFile() {
return $this->moduleExtensionList->getPath('media_library') . '/js/plugins/drupalmedialibrary/plugin.js';
}
/**
* {@inheritdoc}
*/
public function getConfig(Editor $editor) {
// If the editor has not been saved yet, we may not be able to create a
// coherent MediaLibraryState object, which is needed in order to generate
// the required configuration. But, if we're creating a new editor, we don't
// need to do that anyway, so just return an empty array.
if ($editor->isNew()) {
return [];
}
$media_type_ids = $this->mediaTypeStorage->getQuery()->execute();
if ($editor->hasAssociatedFilterFormat()) {
if ($media_embed_filter = $editor->getFilterFormat()->filters()->get('media_embed')) {
// Optionally limit the allowed media types based on the MediaEmbed
// setting. If the setting is empty, do not limit the options.
if (!empty($media_embed_filter->settings['allowed_media_types'])) {
$media_type_ids = array_intersect_key($media_type_ids, $media_embed_filter->settings['allowed_media_types']);
}
}
}
if (in_array('image', $media_type_ids, TRUE)) {
// Due to a bug where the active item styling and the focus styling
// create the visual appearance of two active items, we'll move
// the 'image' media type to first position, so that the focused item and
// the active item are the same.
// This workaround can be removed once this issue is fixed:
// @see https://www.drupal.org/project/drupal/issues/3073799
array_unshift($media_type_ids, 'image');
$media_type_ids = array_unique($media_type_ids);
}
$state = MediaLibraryState::create(
'media_library.opener.editor',
$media_type_ids,
reset($media_type_ids),
1,
['filter_format_id' => $editor->getFilterFormat()->id()]
);
return [
'DrupalMediaLibrary_url' => Url::fromRoute('media_library.ui')
->setOption('query', $state->all())
->toString(TRUE)
->getGeneratedUrl(),
'DrupalMediaLibrary_dialogOptions' => MediaLibraryUiBuilder::dialogOptions(),
];
}
/**
* {@inheritdoc}
*/
public function getButtons() {
return [
'DrupalMediaLibrary' => [
'label' => $this->t('Insert from Media Library'),
'image' => $this->moduleExtensionList->getPath('media_library') . '/js/plugins/drupalmedialibrary/icons/drupalmedialibrary.png',
],
];
}
}
@@ -0,0 +1,932 @@
<?php
namespace Drupal\media_library\Plugin\Field\FieldWidget;
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\SortArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\AnnounceCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
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\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\field_ui\FieldUI;
use Drupal\media\Entity\Media;
use Drupal\media_library\MediaLibraryUiBuilder;
use Drupal\media_library\MediaLibraryState;
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
* Plugin classes are internal.
*/
class MediaLibraryWidget extends WidgetBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
/**
* Entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The current active user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* 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.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current active user.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user = NULL, ModuleHandlerInterface $module_handler = NULL) {
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings);
$this->entityTypeManager = $entity_type_manager;
if (!$current_user) {
@trigger_error('The current_user service must be passed to MediaLibraryWidget::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
$current_user = \Drupal::currentUser();
}
$this->currentUser = $current_user;
if (!$module_handler) {
@trigger_error('The module_handler service must be passed to MediaLibraryWidget::__construct(), it is required before Drupal 9.0.0.', E_USER_DEPRECATED);
$module_handler = \Drupal::moduleHandler();
}
$this->moduleHandler = $module_handler;
}
/**
* {@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'),
$container->get('current_user'),
$container->get('module_handler')
);
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
return $field_definition->getSetting('target_type') === 'media';
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'media_types' => [],
] + parent::defaultSettings();
}
/**
* Gets the enabled media type IDs sorted by weight.
*
* @return string[]
* The media type IDs sorted by weight.
*/
protected function getAllowedMediaTypeIdsSorted() {
// Get the media type IDs sorted by the user in the settings form.
$sorted_media_type_ids = $this->getSetting('media_types');
// Get the configured media types from the field storage.
$handler_settings = $this->getFieldSetting('handler_settings');
// The target bundles will be blank when saving field storage settings,
// when first adding a media reference field.
$allowed_media_type_ids = isset($handler_settings['target_bundles']) ? $handler_settings['target_bundles'] : NULL;
// When there are no allowed media types, return the empty array.
if ($allowed_media_type_ids === []) {
return $allowed_media_type_ids;
}
// When no target bundles are configured for the field, all are allowed.
if ($allowed_media_type_ids === NULL) {
$allowed_media_type_ids = $this->entityTypeManager->getStorage('media_type')->getQuery()->execute();
}
// When the user did not sort the media types, return the media type IDs
// configured for the field.
if (empty($sorted_media_type_ids)) {
return $allowed_media_type_ids;
}
// Some of the media types may no longer exist, and new media types may have
// been added that we don't yet know about. We need to make sure new media
// types are added to the list and remove media types that are no longer
// configured for the field.
$new_media_type_ids = array_diff($allowed_media_type_ids, $sorted_media_type_ids);
// Add new media type IDs to the list.
$sorted_media_type_ids = array_merge($sorted_media_type_ids, array_values($new_media_type_ids));
// Remove media types that are no longer available.
$sorted_media_type_ids = array_intersect($sorted_media_type_ids, $allowed_media_type_ids);
// Make sure the keys are numeric.
return array_values($sorted_media_type_ids);
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$media_type_ids = $this->getAllowedMediaTypeIdsSorted();
if (count($media_type_ids) <= 1) {
return $form;
}
$form['media_types'] = [
'#type' => 'table',
'#header' => [
$this->t('Tab order'),
$this->t('Weight'),
],
'#tabledrag' => [
[
'action' => 'order',
'relationship' => 'sibling',
'group' => 'weight',
],
],
'#value_callback' => [static::class, 'setMediaTypesValue'],
];
$media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($media_type_ids);
$weight = 0;
foreach ($media_types as $media_type_id => $media_type) {
$label = $media_type->label();
$form['media_types'][$media_type_id] = [
'label' => ['#markup' => $label],
'weight' => [
'#type' => 'weight',
'#title' => t('Weight for @title', ['@title' => $label]),
'#title_display' => 'invisible',
'#default_value' => $weight,
'#attributes' => ['class' => ['weight']],
],
'#weight' => $weight,
'#attributes' => ['class' => ['draggable']],
];
$weight++;
}
return $form;
}
/**
* Value callback to optimize the way the media type weights are stored.
*
* The tabledrag functionality needs a specific weight field, but we don't
* want to store this extra weight field in our settings.
*
* @param array $element
* An associative array containing the properties of the element.
* @param mixed $input
* The incoming input to populate the form element. If this is FALSE,
* the element's default value should be returned.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return mixed
* The value to assign to the element.
*/
public static function setMediaTypesValue(array &$element, $input, FormStateInterface $form_state) {
if ($input === FALSE) {
return isset($element['#default_value']) ? $element['#default_value'] : [];
}
// Sort the media types by weight value and set the value in the form state.
uasort($input, 'Drupal\Component\Utility\SortArray::sortByWeightElement');
$sorted_media_type_ids = array_keys($input);
$form_state->setValue($element['#parents'], $sorted_media_type_ids);
// We have to unset the child elements containing the weight fields for each
// media type to stop FormBuilder::doBuildForm() from processing the weight
// fields as well.
foreach ($sorted_media_type_ids as $media_type_id) {
unset($element[$media_type_id]);
}
return $sorted_media_type_ids;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$media_type_labels = [];
$media_types = $this->entityTypeManager->getStorage('media_type')->loadMultiple($this->getAllowedMediaTypeIdsSorted());
if (count($media_types) !== 1) {
foreach ($media_types as $media_type) {
$media_type_labels[] = $media_type->label();
}
$summary[] = t('Tab order: @order', ['@order' => implode(', ', $media_type_labels)]);
}
return $summary;
}
/**
* {@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'];
// Create an ID suffix from the parents to make sure each widget is unique.
$id_suffix = $parents ? '-' . implode('-', $parents) : '';
$field_widget_id = implode(':', array_filter([$field_name, $id_suffix]));
$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' => ['js-media-library-widget'],
],
'#pre_render' => [
[$this, 'preRenderWidget'],
],
'#attached' => [
'library' => ['media_library/widget'],
],
'#theme_wrappers' => [
'fieldset__media_library_widget',
],
];
// When the list of allowed types in the field configuration is null,
// ::getAllowedMediaTypeIdsSorted() returns all existing media types. When
// the list of allowed types is an empty array, we show a message to users
// and ask them to configure the field if they have access.
$allowed_media_type_ids = $this->getAllowedMediaTypeIdsSorted();
if (!$allowed_media_type_ids) {
$element['no_types_message'] = [
'#markup' => $this->getNoMediaTypesAvailableMessage(),
];
return $element;
}
if (empty($referenced_entities)) {
$element['#field_prefix']['empty_selection'] = [
'#markup' => $this->t('No media items are selected.'),
];
}
else {
// @todo Use a <button> link here, and delete
// seven_preprocess_fieldset__media_library_widget(), when
// https://www.drupal.org/project/drupal/issues/2999549 lands.
$element['#field_prefix']['weight_toggle'] = [
'#type' => 'html_tag',
'#tag' => 'button',
'#value' => $this->t('Show media item weights'),
'#attributes' => [
'class' => [
'link',
'js-media-library-widget-toggle-weight',
],
],
];
}
$element['selection'] = [
'#type' => 'container',
'#theme_wrappers' => [
'container__media_library_widget_selection',
],
'#attributes' => [
'class' => [
'js-media-library-selection',
],
],
];
foreach ($referenced_entities as $delta => $media_item) {
$element['selection'][$delta] = [
'#theme' => 'media_library_item__widget',
'#attributes' => [
'class' => [
'js-media-library-item',
],
// Add the tabindex '-1' to allow the focus to be shifted to the next
// media item when an item is removed. We set focus to the container
// because we do not want to set focus to the remove button
// automatically.
// @see ::updateWidget()
'tabindex' => '-1',
// Add a data attribute containing the delta to allow us to easily
// shift the focus to a specific media item.
// @see ::updateWidget()
'data-media-library-item-delta' => $delta,
],
'remove_button' => [
'#type' => 'submit',
'#name' => $field_name . '-' . $delta . '-media-library-remove-button' . $id_suffix,
'#value' => $this->t('Remove'),
'#media_id' => $media_item->id(),
'#attributes' => [
'aria-label' => $this->t('Remove @label', ['@label' => $media_item->label()]),
],
'#ajax' => [
'callback' => [static::class, 'updateWidget'],
'wrapper' => $wrapper_id,
'progress' => [
'type' => 'throbber',
'message' => $this->t('Removing @label.', ['@label' => $media_item->label()]),
],
],
'#submit' => [[static::class, 'removeItem']],
// Prevent errors in other widgets from preventing removal.
'#limit_validation_errors' => $limit_validation_errors,
],
// @todo Make the view mode configurable in https://www.drupal.org/project/drupal/issues/2971209
'rendered_entity' => $view_builder->view($media_item, 'media_library'),
'target_id' => [
'#type' => 'hidden',
'#value' => $media_item->id(),
],
// This hidden value can be toggled visible for accessibility.
'weight' => [
'#type' => 'number',
'#theme' => 'input__number__media_library_item_weight',
'#title' => $this->t('Weight'),
'#default_value' => $delta,
'#attributes' => [
'class' => [
'js-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.');
}
// Add a line break between the field message and the cardinality message.
if (!empty($element['#description'])) {
$element['#description'] .= '<br />';
}
$element['#description'] .= $cardinality_message;
}
// Create a new media library URL with the correct state parameters.
$selected_type_id = reset($allowed_media_type_ids);
$remaining = $cardinality_unlimited ? FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED : $remaining;
// This particular media library opener needs some extra metadata for its
// \Drupal\media_library\MediaLibraryOpenerInterface::getSelectionResponse()
// to be able to target the element whose 'data-media-library-widget-value'
// attribute is the same as $field_widget_id. The entity ID, entity type ID,
// bundle, field name are used for access checking.
$entity = $items->getEntity();
$opener_parameters = [
'field_widget_id' => $field_widget_id,
'entity_type_id' => $entity->getEntityTypeId(),
'bundle' => $entity->bundle(),
'field_name' => $field_name,
];
// Only add the entity ID when we actually have one. The entity ID needs to
// be a string to ensure that the media library state generates its
// tamper-proof hash in a consistent way.
if (!$entity->isNew()) {
$opener_parameters['entity_id'] = (string) $entity->id();
}
$state = MediaLibraryState::create('media_library.opener.field_widget', $allowed_media_type_ids, $selected_type_id, $remaining, $opener_parameters);
// Add a button that will load the Media library in a modal using AJAX.
$element['open_button'] = [
'#type' => 'button',
'#value' => $this->t('Add media'),
'#name' => $field_name . '-media-library-open-button' . $id_suffix,
'#attributes' => [
'class' => [
'js-media-library-open-button',
],
// The jQuery UI dialog automatically moves focus to the first :tabbable
// element of the modal, so we need to disable refocus on the button.
'data-disable-refocus' => 'true',
],
'#media_library_state' => $state,
'#ajax' => [
'callback' => [static::class, 'openMediaLibrary'],
'progress' => [
'type' => 'throbber',
'message' => $this->t('Opening media library.'),
],
],
// Allow the media library to be opened even if there are form errors.
'#limit_validation_errors' => [],
];
// When the user returns from the modal to the widget, we want to shift the
// focus back to the open button. If the user is not allowed to add more
// items, the button needs to be disabled. Since we can't shift the focus to
// disabled elements, the focus is set back to the open button via
// JavaScript by adding the 'data-disabled-focus' attribute.
// @see Drupal.behaviors.MediaLibraryWidgetDisableButton
if (!$cardinality_unlimited && $remaining === 0) {
$element['open_button']['#attributes']['data-disabled-focus'] = 'true';
$element['open_button']['#attributes']['class'][] = 'visually-hidden';
}
// 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_widget_id,
],
];
// 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,
'progress' => [
'type' => 'throbber',
'message' => $this->t('Adding selection.'),
],
],
'#attributes' => [
'data-media-library-widget-update' => $field_widget_id,
'class' => ['js-hide'],
],
'#validate' => [[static::class, 'validateItems']],
'#submit' => [[static::class, 'addItems']],
// We need to prevent the widget from being validated when no media items
// are selected. When a media field is added in a subform, entity
// validation is triggered in EntityFormDisplay::validateFormValues().
// Since the media item is not added to the form yet, this triggers errors
// for required media fields.
'#limit_validation_errors' => !empty($referenced_entities) ? $limit_validation_errors : [],
];
return $element;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['preRenderWidget'];
}
/**
* Prepares the widget's render element for rendering.
*
* @param array $element
* The element to transform.
*
* @return array
* The transformed element.
*
* @see ::formElement()
*/
public function preRenderWidget(array $element) {
if (isset($element['open_button'])) {
$element['#field_suffix']['open_button'] = $element['open_button'];
unset($element['open_button']);
}
return $element;
}
/**
* Gets the message to display when there are no allowed media types.
*
* @return \Drupal\Component\Render\MarkupInterface
* The message to display when there are no allowed media types.
*/
protected function getNoMediaTypesAvailableMessage() {
$entity_type_id = $this->fieldDefinition->getTargetEntityTypeId();
$default_message = $this->t('There are no allowed media types configured for this field. Please contact the site administrator.');
// Show the default message if the user does not have the permissions to
// configure the fields for the entity type.
if (!$this->currentUser->hasPermission("administer $entity_type_id fields")) {
return $default_message;
}
// Show a message for privileged users to configure the field if the Field
// UI module is not enabled.
if (!$this->moduleHandler->moduleExists('field_ui')) {
return $this->t('There are no allowed media types configured for this field. Edit the field settings to select the allowed media types.');
}
// Add a link to the message to configure the field if the Field UI module
// is enabled.
$route_parameters = FieldUI::getRouteBundleParameter($this->entityTypeManager->getDefinition($entity_type_id), $this->fieldDefinition->getTargetBundle());
$route_parameters['field_config'] = $this->fieldDefinition->id();
$url = Url::fromRoute('entity.field_config.' . $entity_type_id . '_field_edit_form', $route_parameters);
if ($url->access($this->currentUser)) {
return $this->t('There are no allowed media types configured for this field. <a href=":url">Edit the field settings</a> to select the allowed media types.', [
':url' => $url->toString(),
]);
}
// If the user for some reason doesn't have access to the Field UI, fall
// back to the default message.
return $default_message;
}
/**
* {@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 \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to update the selection.
*/
public static function updateWidget(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$wrapper_id = $triggering_element['#ajax']['wrapper'];
// This callback is either invoked from the remove button or the update
// button, which have different nesting levels.
$is_remove_button = end($triggering_element['#parents']) === 'remove_button';
$length = $is_remove_button ? -3 : -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'] = '';
$field_state = static::getFieldState($element, $form_state);
// Announce the updated content to screen readers.
if ($is_remove_button) {
$announcement = t('@label has been removed.', [
'@label' => Media::load($field_state['removed_item_id'])->label(),
]);
}
else {
$new_items = count(static::getNewMediaItems($element, $form_state));
$announcement = \Drupal::translation()->formatPlural($new_items, 'Added one media item.', 'Added @count media items.');
}
$response = new AjaxResponse();
$response->addCommand(new ReplaceCommand("#$wrapper_id", $element));
$response->addCommand(new AnnounceCommand($announcement));
// When the remove button is clicked, shift focus to the next remove button.
// When the last item is deleted, we no longer have a selection and shift
// the focus to the open button.
$removed_last = $is_remove_button && !count($field_state['items']);
if ($is_remove_button && !$removed_last) {
// Find the next media item by weight. The weight of the removed item is
// added to the field state when it is removed in ::removeItem(). If there
// is no item with a bigger weight, we automatically shift the focus to
// the previous media item.
// @see ::removeItem()
$removed_item_weight = $field_state['removed_item_weight'];
$delta_to_focus = 0;
foreach ($field_state['items'] as $delta => $item_fields) {
$delta_to_focus = $delta;
if ($item_fields['weight'] > $removed_item_weight) {
// Stop directly when we find an item with a bigger weight. We also
// have to subtract 1 from the delta in this case, since the delta's
// are renumbered when rebuilding the form.
$delta_to_focus--;
break;
}
}
$response->addCommand(new InvokeCommand("#$wrapper_id [data-media-library-item-delta=$delta_to_focus]", 'focus'));
}
// Shift focus to the open button if the user removed the last selected
// item, or when the user has added items to the selection and is allowed to
// select more items. When the user is not allowed to add more items, the
// button needs to be disabled. Since we can't shift the focus to disabled
// elements, the focus is set via JavaScript by adding the
// 'data-disabled-focus' attribute and we also don't want to set the focus
// here.
// @see Drupal.behaviors.MediaLibraryWidgetDisableButton
elseif ($removed_last || (!$is_remove_button && !isset($element['open_button']['#attributes']['data-disabled-focus']))) {
$response->addCommand(new InvokeCommand("#$wrapper_id .js-media-library-open-button", 'focus'));
}
return $response;
}
/**
* 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, -3);
$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);
// Get the delta of the item being removed.
$delta = array_slice($triggering_element['#array_parents'], -2, 1)[0];
if (isset($values['selection'][$delta])) {
// Add the weight of the removed item to the field state so we can shift
// focus to the next/previous item in an easy way.
$field_state['removed_item_weight'] = $values['selection'][$delta]['weight'];
$field_state['removed_item_id'] = $triggering_element['#media_id'];
unset($values['selection'][$delta]);
$field_state['items'] = $values['selection'];
static::setFieldState($element, $form_state, $field_state);
}
$form_state->setRebuild();
}
/**
* AJAX callback to open the library modal.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response to open the media library.
*/
public static function openMediaLibrary(array $form, FormStateInterface $form_state) {
$triggering_element = $form_state->getTriggeringElement();
$library_ui = \Drupal::service('media_library.ui_builder')->buildUi($triggering_element['#media_library_state']);
$dialog_options = MediaLibraryUiBuilder::dialogOptions();
return (new AjaxResponse())
->addCommand(new OpenModalDialogCommand($dialog_options['title'], $library_ui, $dialog_options));
}
/**
* 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 addItems(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)) {
// Get the weight of the last items and count from there.
$last_element = end($field_state['items']);
$weight = $last_element ? $last_element['weight'] : 0;
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. We need to use the
// actual user input, since when #limit_validation_errors is used, the
// unvalidated user input is not added to the form state.
// @see FormValidator::handleErrorsWithLimitedValidation()
$values = $form_state->getUserInput();
$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) {
// Default to using the current selection if the form is new.
$path = $element['#parents'];
// We need to use the actual user input, since when #limit_validation_errors
// is used, the unvalidated user input is not added to the form state.
// @see FormValidator::handleErrorsWithLimitedValidation()
$values = NestedArray::getValue($form_state->getUserInput(), $path);
$selection = isset($values['selection']) ? $values['selection'] : [];
$widget_state = static::getWidgetState($element['#field_parents'], $element['#field_name'], $form_state);
$widget_state['items'] = isset($widget_state['items']) ? $widget_state['items'] : $selection;
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);
}
}
@@ -0,0 +1,153 @@
<?php
namespace Drupal\media_library\Plugin\views\field;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Form\FormBuilderInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\media_library\MediaLibraryState;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\Render\ViewsRenderPipelineMarkup;
use Drupal\views\ResultRow;
use Symfony\Component\HttpFoundation\Request;
/**
* Defines a field that outputs a checkbox and form for selecting media.
*
* @ViewsField("media_library_select_form")
*
* @internal
* Plugin classes are 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) {
$form['#attributes']['class'] = ['js-media-library-views-form'];
// Add an attribute that identifies the media type displayed in the form.
if (isset($this->view->args[0])) {
$form['#attributes']['data-drupal-media-type'] = $this->view->args[0];
}
// 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(),
];
}
// The selection is persistent across different pages in the media library
// and populated via JavaScript.
$selection_field_id = $this->options['id'] . '_selection';
$form[$selection_field_id] = [
'#type' => 'hidden',
'#attributes' => [
// This is used to identify the hidden field in the form via JavaScript.
'id' => 'media-library-modal-selection',
],
];
// @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.
$query = $this->view->getRequest()->query->all();
$query[FormBuilderInterface::AJAX_FORM_REQUEST] = TRUE;
$query['views_display_id'] = $this->view->getDisplay()->display['id'];
$form['actions']['submit']['#ajax'] = [
'url' => Url::fromRoute('media_library.ui'),
'options' => [
'query' => $query,
],
'callback' => [static::class, 'updateWidget'],
];
$form['actions']['submit']['#value'] = $this->t('Insert selected');
$form['actions']['submit']['#button_type'] = 'primary';
$form['actions']['submit']['#field_id'] = $selection_field_id;
// By default, the AJAX system tries to move the focus back to the element
// that triggered the AJAX request. Since the media library is closed after
// clicking the select button, the focus can't be moved back. We need to set
// the 'data-disable-refocus' attribute to prevent the AJAX system from
// moving focus to a random element. The select button triggers an update in
// the opener, and the opener should be responsible for moving the focus. An
// example of this can be seen in MediaLibraryWidget::updateWidget().
// @see \Drupal\media_library\Plugin\Field\FieldWidget\MediaLibraryWidget::updateWidget()
$form['actions']['submit']['#attributes']['data-disable-refocus'] = 'true';
}
/**
* 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.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @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, Request $request) {
$field_id = $form_state->getTriggeringElement()['#field_id'];
$selected_ids = $form_state->getValue($field_id);
$selected_ids = $selected_ids ? array_filter(explode(',', $selected_ids)) : [];
// Allow the opener service to handle the selection.
$state = MediaLibraryState::fromRequest($request);
return \Drupal::service('media_library.opener_resolver')
->get($state)
->getSelectionResponse($state, $selected_ids)
->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;
}
}
@@ -0,0 +1,30 @@
<?php
namespace Drupal\media_library\Routing;
use Drupal\Core\Routing\RouteSubscriberBase;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for media library routes.
*
* @internal
* Tagged services are internal.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
// Add the media library UI access checks to the widget displays of the
// media library view.
if ($route = $collection->get('view.media_library.widget')) {
$route->addRequirements(['_custom_access' => 'media_library.ui_builder:checkAccess']);
}
if ($route = $collection->get('view.media_library.widget_table')) {
$route->addRequirements(['_custom_access' => 'media_library.ui_builder:checkAccess']);
}
}
}
@@ -0,0 +1,49 @@
{#
/**
* @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()
* @see media_library_preprocess_media()
*
* @ingroup themeable
*/
#}
<article{{ attributes }}>
{% if content %}
<div{{ preview_attributes.addClass('js-media-library-item-preview') }}>
{{ content|without('name') }}
</div>
{% if not status %}
{{ "unpublished" | t }}
{% endif %}
<div{{ metadata_attributes }}>
{{ name }}
</div>
{% endif %}
</article>
@@ -0,0 +1,22 @@
{#
/**
* @file
* Default theme implementation of a media library item.
*
* This is used when displaying selected media items, either in the field
* widget or in the "Additional selected media" area when adding new
* media items in the media library modal dialog.
*
* Available variables:
* - attributes: HTML attributes for the containing element.
* - content: The content of the media library item, plus any additional
* fields or elements surrounding it.
*
* @see template_preprocess_media_library_item()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ content }}
</div>
@@ -0,0 +1,21 @@
{#
/**
* @file
* Default theme implementation of a container used to wrap the media library's
* modal dialog interface.
*
* Available variables:
* - attributes: HTML attributes for the containing element.
* - menu: The menu of availble media types to choose from.
* - content: The form to add new media items, followed by the grid or table of
* existing media items to choose from.
*
* @see template_preprocess_media_library_wrapper()
*
* @ingroup themeable
*/
#}
<div{{ attributes }}>
{{ menu }}
{{ content }}
</div>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,59 @@
langcode: en
status: true
dependencies:
config:
- field.field.media.type_five.field_media_test_oembed_video
- media.type.type_five
module:
- path
id: media.type_five.default
targetEntityType: media
bundle: type_five
mode: default
content:
created:
type: datetime_timestamp
weight: 10
region: content
settings: { }
third_party_settings: { }
field_media_test_oembed_video:
type: oembed_textfield
weight: 0
settings:
size: 60
placeholder: ''
third_party_settings: { }
region: content
name:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
status:
type: boolean_checkbox
settings:
display_label: true
weight: 100
region: content
third_party_settings: { }
uid:
type: entity_reference_autocomplete
weight: 5
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
region: content
third_party_settings: { }
hidden: { }
@@ -0,0 +1,33 @@
langcode: en
status: true
dependencies:
config:
- core.entity_form_mode.media.media_library
- field.field.media.type_five.field_media_test_oembed_video
- media.type.type_five
id: media.type_five.media_library
targetEntityType: media
bundle: type_five
mode: media_library
content:
field_media_test_oembed_video:
weight: 1
settings:
size: 60
placeholder: ''
third_party_settings: { }
type: oembed_textfield
region: content
name:
type: string_textfield
weight: 0
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
hidden:
created: true
path: true
status: true
uid: true
@@ -0,0 +1,70 @@
langcode: en
status: true
dependencies:
config:
- field.field.media.type_four.field_media_test_image
- field.field.media.type_four.field_media_extra_image
- image.style.medium
- media.type.type_four
module:
- image
- path
id: media.type_four.default
targetEntityType: media
bundle: type_four
mode: default
content:
created:
type: datetime_timestamp
weight: 10
region: content
settings: { }
third_party_settings: { }
field_media_test_image:
weight: 0
settings:
progress_indicator: throbber
preview_image_style: medium
third_party_settings: { }
type: image_image
region: content
field_media_extra_image:
weight: 1
settings:
progress_indicator: throbber
preview_image_style: medium
third_party_settings: { }
type: image_image
region: content
name:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
status:
type: boolean_checkbox
settings:
display_label: true
weight: 100
region: content
third_party_settings: { }
uid:
type: entity_reference_autocomplete
weight: 5
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
region: content
third_party_settings: { }
hidden: { }
@@ -0,0 +1,44 @@
langcode: en
status: true
dependencies:
config:
- core.entity_form_mode.media.media_library
- field.field.media.type_four.field_media_test_image
- image.style.thumbnail
- media.type.type_four
module:
- image
id: media.type_four.media_library
targetEntityType: media
bundle: type_four
mode: media_library
content:
field_media_test_image:
weight: 2
settings:
progress_indicator: throbber
preview_image_style: thumbnail
third_party_settings: { }
type: image_image
region: content
field_media_extra_image:
weight: 1
settings:
progress_indicator: throbber
preview_image_style: medium
third_party_settings: { }
type: image_image
region: content
name:
type: string_textfield
weight: 0
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
hidden:
created: true
path: true
status: true
uid: true
@@ -0,0 +1,44 @@
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
match_limit: 10
size: 60
placeholder: ''
region: content
third_party_settings: { }
hidden: { }
@@ -0,0 +1,61 @@
langcode: en
status: true
dependencies:
config:
- field.field.media.type_three.field_media_test_image
- image.style.medium
- media.type.type_three
module:
- image
- path
id: media.type_three.default
targetEntityType: media
bundle: type_three
mode: default
content:
created:
type: datetime_timestamp
weight: 10
region: content
settings: { }
third_party_settings: { }
field_media_test_image:
weight: 0
settings:
progress_indicator: throbber
preview_image_style: medium
third_party_settings: { }
type: image_image
region: content
name:
type: string_textfield
weight: -5
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
path:
type: path
weight: 30
region: content
settings: { }
third_party_settings: { }
status:
type: boolean_checkbox
settings:
display_label: true
weight: 100
region: content
third_party_settings: { }
uid:
type: entity_reference_autocomplete
weight: 5
settings:
match_operator: CONTAINS
match_limit: 10
size: 60
placeholder: ''
region: content
third_party_settings: { }
hidden: { }
@@ -0,0 +1,36 @@
langcode: en
status: true
dependencies:
config:
- core.entity_form_mode.media.media_library
- field.field.media.type_three.field_media_test_image
- image.style.thumbnail
- media.type.type_three
module:
- image
id: media.type_three.media_library
targetEntityType: media
bundle: type_three
mode: media_library
content:
field_media_test_image:
weight: 1
settings:
progress_indicator: throbber
preview_image_style: thumbnail
third_party_settings: { }
type: image_image
region: content
name:
type: string_textfield
weight: 0
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
hidden:
created: true
path: true
status: true
uid: true
@@ -0,0 +1,44 @@
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
match_limit: 10
size: 60
placeholder: ''
region: content
third_party_settings: { }
hidden: { }
@@ -0,0 +1,103 @@
langcode: en
status: true
dependencies:
config:
- field.field.node.basic_page.field_twin_media
- field.field.node.basic_page.field_single_media_type
- field.field.node.basic_page.field_unlimited_media
- field.field.node.basic_page.field_noadd_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:
media_types:
- type_three
- type_one
- type_two
- type_four
third_party_settings: { }
region: content
field_single_media_type:
type: media_library_widget
weight: 124
settings: { }
third_party_settings: { }
region: content
field_unlimited_media:
type: media_library_widget
weight: 121
settings: { }
third_party_settings: { }
region: content
field_noadd_media:
type: media_library_widget
weight: 123
settings: { }
third_party_settings: { }
region: content
field_null_types_media:
type: media_library_widget
weight: 125
settings: { }
third_party_settings: { }
region: content
field_empty_types_media:
type: media_library_widget
weight: 126
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
match_limit: 10
size: 60
placeholder: ''
region: content
third_party_settings: { }
hidden: { }
@@ -0,0 +1,50 @@
langcode: en
status: true
dependencies:
config:
- field.field.media.type_five.field_media_test_oembed_video
- media.type.type_five
module:
- media
id: media.type_five.default
targetEntityType: media
bundle: type_five
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_oembed_video:
weight: 6
label: hidden
settings:
max_width: 0
max_height: 0
third_party_settings: { }
type: oembed
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:
name: true
@@ -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: { }
@@ -0,0 +1,36 @@
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.media_library
targetEntityType: media
bundle: type_one
mode: media_library
content:
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: { }
hidden:
created: true
name: true
uid: true
@@ -0,0 +1,52 @@
langcode: en
status: true
dependencies:
config:
- field.field.media.type_three.field_media_test_image
- image.style.thumbnail
- media.type.type_three
module:
- image
- user
id: media.type_three.default
targetEntityType: media
bundle: type_three
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_image:
weight: 6
label: above
settings:
image_style: ''
image_link: ''
third_party_settings: { }
type: image
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:
name: true
@@ -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: { }
@@ -0,0 +1,36 @@
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.media_library
targetEntityType: media
bundle: type_two
mode: media_library
content:
field_media_test_1:
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: { }
hidden:
created: true
name: true
uid: true
@@ -0,0 +1,75 @@
langcode: en
status: true
dependencies:
config:
- field.field.node.basic_page.field_twin_media
- field.field.node.basic_page.field_unlimited_media
- field.field.node.basic_page.field_noadd_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_single_media_type:
type: entity_reference_entity_view
weight: 101
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
field_noadd_media:
type: entity_reference_entity_view
weight: 103
label: above
settings:
view_mode: default
link: false
third_party_settings: { }
region: content
field_empty_types_media:
type: entity_reference_entity_view
weight: 104
label: above
settings:
view_mode: default
link: false
third_party_settings: { }
region: content
field_null_types_media:
type: entity_reference_entity_view
weight: 105
label: above
settings:
view_mode: default
link: false
third_party_settings: { }
region: content
links:
weight: 100
settings: { }
third_party_settings: { }
region: content
hidden: { }
@@ -0,0 +1,18 @@
langcode: en
status: true
dependencies:
config:
- field.storage.media.field_media_test_oembed_video
- media.type.type_five
id: media.type_five.field_media_test_oembed_video
field_name: field_media_test_oembed_video
entity_type: media
bundle: type_five
label: 'Video URL'
description: ''
required: true
translatable: true
default_value: { }
default_value_callback: ''
settings: { }
field_type: string
@@ -0,0 +1,37 @@
langcode: en
status: true
dependencies:
config:
- field.storage.media.field_media_extra_image
- media.type.type_four
module:
- image
id: media.type_three.field_media_extra_image
field_name: field_media_extra_image
entity_type: media
bundle: type_four
label: Extra Image
description: ''
required: false
translatable: true
default_value: { }
default_value_callback: ''
settings:
file_extensions: 'jpg'
alt_field: false
alt_field_required: false
title_field: false
title_field_required: false
max_resolution: ''
min_resolution: ''
default_image:
uuid: null
alt: ''
title: ''
width: null
height: null
file_directory: 'type-four-extra-dir'
max_filesize: ''
handler: 'default:file'
handler_settings: { }
field_type: image
@@ -0,0 +1,37 @@
langcode: en
status: true
dependencies:
config:
- field.storage.media.field_media_test_image
- media.type.type_four
module:
- image
id: media.type_three.field_media_test_image
field_name: field_media_test_image
entity_type: media
bundle: type_four
label: Image
description: ''
required: true
translatable: true
default_value: { }
default_value_callback: ''
settings:
file_extensions: 'jpg'
alt_field: true
alt_field_required: true
title_field: false
title_field_required: false
max_resolution: ''
min_resolution: ''
default_image:
uuid: null
alt: ''
title: ''
width: null
height: null
file_directory: 'type-four-dir'
max_filesize: ''
handler: 'default:file'
handler_settings: { }
field_type: image
@@ -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
@@ -0,0 +1,37 @@
langcode: en
status: true
dependencies:
config:
- field.storage.media.field_media_test_image
- media.type.type_three
module:
- image
id: media.type_three.field_media_test_image
field_name: field_media_test_image
entity_type: media
bundle: type_three
label: Image
description: ''
required: true
translatable: true
default_value: { }
default_value_callback: ''
settings:
file_extensions: 'png gif jpg jpeg'
alt_field: true
alt_field_required: true
title_field: false
title_field_required: false
max_resolution: ''
min_resolution: ''
default_image:
uuid: null
alt: ''
title: ''
width: null
height: null
file_directory: 'type-three-dir'
max_filesize: ''
handler: 'default:file'
handler_settings: { }
field_type: image
@@ -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
@@ -0,0 +1,25 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_empty_types_media
- node.type.basic_page
id: node.basic_page.field_empty_types_media
field_name: field_empty_types_media
entity_type: node
bundle: basic_page
label: 'Empty types media'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:media'
handler_settings:
target_bundles: { }
sort:
field: _none
auto_create: false
auto_create_bundle: file
field_type: entity_reference
@@ -0,0 +1,29 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_noadd_media
- media.type.type_one
- media.type.type_two
- node.type.basic_page
id: node.basic_page.field_noadd_media
field_name: field_noadd_media
entity_type: node
bundle: basic_page
label: 'No add 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
@@ -0,0 +1,25 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_null_types_media
- node.type.basic_page
id: node.basic_page.field_null_types_media
field_name: field_null_types_media
entity_type: node
bundle: basic_page
label: 'Null types media'
description: ''
required: false
translatable: false
default_value: { }
default_value_callback: ''
settings:
handler: 'default:media'
handler_settings:
target_bundles: null
sort:
field: _none
auto_create: false
auto_create_bundle: file
field_type: entity_reference
@@ -0,0 +1,28 @@
langcode: en
status: true
dependencies:
config:
- field.storage.node.field_single_media_type
- media.type.type_one
- media.type.type_two
- node.type.basic_page
id: node.basic_page.field_single_media_type
field_name: field_single_media_type
entity_type: node
bundle: basic_page
label: 'Single media type'
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: file
field_type: entity_reference
@@ -0,0 +1,31 @@
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
type_three: type_three
type_four: type_four
sort:
field: _none
auto_create: false
auto_create_bundle: file
field_type: entity_reference
@@ -0,0 +1,29 @@
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
type_three: type_three
type_five: type_five
sort:
field: _none
auto_create: false
auto_create_bundle: audio
field_type: entity_reference
@@ -0,0 +1,29 @@
langcode: en
status: true
dependencies:
module:
- file
- image
- media
id: media.field_media_extra_image
field_name: field_media_extra_image
entity_type: media
type: image
settings:
default_image:
uuid: null
alt: ''
title: ''
width: null
height: null
target_type: file
display_field: false
display_default: false
uri_scheme: public
module: image
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
@@ -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
@@ -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
@@ -0,0 +1,29 @@
langcode: en
status: true
dependencies:
module:
- file
- image
- media
id: media.field_media_test_image
field_name: field_media_test_image
entity_type: media
type: image
settings:
default_image:
uuid: null
alt: ''
title: ''
width: null
height: null
target_type: file
display_field: false
display_default: false
uri_scheme: public
module: image
locked: false
cardinality: 1
translatable: true
indexes: { }
persist_with_no_fields: false
custom_storage: false
@@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
module:
- media
id: media.field_media_test_oembed_video
field_name: field_media_test_oembed_video
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
@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- media
- node
id: node.field_empty_types_media
field_name: field_empty_types_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
@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- media
- node
id: node.field_noadd_media
field_name: field_noadd_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
@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- media
- node
id: node.field_null_types_media
field_name: field_null_types_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
@@ -0,0 +1,19 @@
langcode: en
status: true
dependencies:
module:
- media
- node
id: node.field_single_media_type
field_name: field_single_media_type
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
@@ -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
@@ -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
@@ -0,0 +1,16 @@
langcode: en
status: true
dependencies: { }
id: type_five
label: 'Type Five'
description: ''
source: 'oembed:video'
queue_thumbnail_downloads: false
new_revision: false
source_configuration:
thumbnails_directory: 'public://oembed_thumbnails'
providers:
- YouTube
- Vimeo
source_field: field_media_test_oembed_video
field_map: { }
@@ -0,0 +1,12 @@
langcode: en
status: true
dependencies: { }
id: type_four
label: 'Type Four'
description: ''
source: image
queue_thumbnail_downloads: false
new_revision: false
source_configuration:
source_field: field_media_test_image
field_map: { }
@@ -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: { }
@@ -0,0 +1,12 @@
langcode: en
status: true
dependencies: { }
id: type_three
label: 'Type Three'
description: ''
source: image
queue_thumbnail_downloads: false
new_revision: false
source_configuration:
source_field: field_media_test_image
field_map: { }
@@ -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: { }
@@ -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
@@ -0,0 +1,12 @@
name: 'Media Library test'
type: module
description: 'Test module for Media Library.'
package: Testing
core: 8.x
dependencies:
- drupal:image
- drupal:media_library
- drupal:media_test_source
- drupal:menu_ui
- drupal:node
- drupal:path

Some files were not shown because too many files have changed in this diff Show More