Commit 92390915 authored by xjm's avatar xjm

Issue #3082690 by phenaproxima, bnjmnm, oknate, JeroenT, effulgentsia, xjm,...

Issue #3082690 by phenaproxima, bnjmnm, oknate, JeroenT, effulgentsia, xjm, lauriii, webchick, seanB, andrewmacpherson, Wim Leers, DyanneNova, Gábor Hojtsy, cboyden, peterx, rainbreaw, jan.stoeckler, shaal, annagaz, FeyP, chr.fritsch, marcoscano, samuel.mortenson, Berdir, webflo: Mark Media Library as a stable core module

(cherry picked from commit 946835f4)
parent 51adbc6a
......@@ -283,6 +283,10 @@ Media
- Christian Fritsch 'chr.fritsch' https://www.drupal.org/u/chr.fritsch
- Adam Globus-Hoenich 'phenaproxima' https://www.drupal.org/u/phenaproxima
Media Library
- Sean Blommaert 'seanB' https://www.drupal.org/u/seanb
- Adam Globus-Hoenich 'phenaproxima' https://www.drupal.org/u/phenaproxima
Menu
- Daniel Wehner 'dawehner' https://www.drupal.org/u/dawehner
- Peter Wolanin 'pwolanin' https://www.drupal.org/u/pwolanin
......
......@@ -73,7 +73,7 @@ display:
type: default
options:
grouping: { }
row_class: 'media-library-item media-library-item--grid js-media-library-item js-click-to-select'
row_class: ''
default_row_class: true
row:
type: fields
......@@ -120,7 +120,7 @@ display:
preserve_tags: ''
html: false
element_type: ''
element_class: js-click-to-select-checkbox media-library-item__click-to-select-checkbox
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
......@@ -173,7 +173,7 @@ display:
preserve_tags: ''
html: false
element_type: ''
element_class: media-library-item__content
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
......@@ -468,7 +468,7 @@ display:
relationships: { }
display_extenders: { }
use_ajax: true
css_class: 'media-library-view js-media-library-view'
css_class: ''
cache_metadata:
max-age: 0
contexts:
......@@ -525,7 +525,7 @@ display:
preserve_tags: ''
html: false
element_type: ''
element_class: js-click-to-select-checkbox media-library-item__click-to-select-checkbox
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
......@@ -627,7 +627,7 @@ display:
trim_whitespace: false
alt: 'Edit {{ name }}'
rel: ''
link_class: media-library-item__edit
link_class: ''
prefix: ''
suffix: ''
target: ''
......@@ -680,7 +680,7 @@ display:
trim_whitespace: false
alt: 'Delete {{ name }}'
rel: ''
link_class: media-library-item__remove
link_class: ''
prefix: ''
suffix: ''
target: ''
......@@ -749,7 +749,7 @@ display:
preserve_tags: ''
html: false
element_type: ''
element_class: media-library-item__content
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
......@@ -786,10 +786,10 @@ display:
display_extenders: { }
path: admin/content/media-widget
fields:
rendered_entity:
id: rendered_entity
media_library_select_form:
id: media_library_select_form
table: media
field: rendered_entity
field: media_library_select_form
relationship: none
group_type: group
admin_label: ''
......@@ -823,7 +823,7 @@ display:
preserve_tags: ''
html: false
element_type: ''
element_class: media-library-item__content
element_class: ''
element_label_type: ''
element_label_class: ''
element_label_colon: false
......@@ -834,13 +834,12 @@ display:
hide_empty: false
empty_zero: false
hide_alter_empty: true
view_mode: media_library
entity_type: media
plugin_id: rendered_entity
media_library_select_form:
id: media_library_select_form
plugin_id: media_library_select_form
rendered_entity:
id: rendered_entity
table: media
field: media_library_select_form
field: rendered_entity
relationship: none
group_type: group
admin_label: ''
......@@ -879,14 +878,15 @@ display:
element_label_class: ''
element_label_colon: false
element_wrapper_type: ''
element_wrapper_class: js-click-to-select-checkbox media-library-item__click-to-select-checkbox
element_wrapper_class: ''
element_default_classes: true
empty: ''
hide_empty: false
empty_zero: false
hide_alter_empty: true
view_mode: media_library
entity_type: media
plugin_id: media_library_select_form
plugin_id: rendered_entity
defaults:
fields: false
access: false
......@@ -1086,7 +1086,7 @@ display:
label: 'Table'
plugin_id: display_link
empty: true
css_class: 'media-library-view js-media-library-view media-library-view--widget'
css_class: ''
rendering_language: '***LANGUAGE_language_interface***'
cache_metadata:
max-age: -1
......@@ -1131,7 +1131,7 @@ display:
relationship: none
entity_type: media
plugin_id: media_library_select_form
element_wrapper_class: js-click-to-select-checkbox media-library-item__click-to-select-checkbox
element_wrapper_class: ''
element_class: ''
thumbnail__target_id:
id: thumbnail__target_id
......@@ -1374,7 +1374,7 @@ display:
label: 'Table'
plugin_id: display_link
empty: true
css_class: 'media-library-view js-media-library-view media-library-view--widget'
css_class: ''
rendering_language: '***LANGUAGE_language_interface***'
cache_metadata:
max-age: -1
......
/**
* @file media_library.module.css
*/
/**
* By default, the dialog is too narrow to be usable.
* @see Drupal.ckeditor.openDialog()
*/
.ui-dialog--narrow.media-library-widget-modal {
max-width: 75%;
}
.media-library-wrapper {
display: flex;
}
.media-library-menu {
margin: 0;
padding: 0;
}
/* @todo Use a class instead of the li element.
https://www.drupal.org/project/drupal/issues/3029227 */
.media-library-menu li {
padding: 0;
list-style: none;
}
.media-library-views-form > .form-actions {
flex-basis: 100%;
}
.media-library-views-form,
.media-library-selection,
.media-library-add-form__selected-media .details-wrapper,
.media-library-views-form__bulk_form,
.media-library-view .form--inline {
display: flex;
flex-wrap: wrap;
}
.media-library-views-form__header {
flex-basis: 100%;
}
.media-library-item {
position: relative;
}
.media-library-item__click-to-select-trigger {
overflow: hidden;
height: 100%;
cursor: pointer;
}
.media-library-view .form-actions {
align-self: flex-end;
}
.media-library-item__click-to-select-checkbox {
position: absolute;
z-index: 1;
top: 16px;
left: 16px; /* LTR */
display: block;
}
[dir="rtl"] .media-library-item__click-to-select-checkbox {
right: 16px;
left: auto;
}
.media-library-item__status {
position: absolute;
top: 40px;
left: 5px; /* LTR */
pointer-events: none;
}
[dir="rtl"] .media-library-item__status {
right: 5px;
left: auto;
}
.media-library-select-all {
flex-basis: 100%;
width: 100%;
}
.media-library-view--widget .media-library-select-all {
display: none;
}
.media-library-item--disabled {
pointer-events: none;
}
.media-library-selection .media-library-item__preview {
cursor: move;
}
.media-library-widget-modal .ui-dialog-buttonpane {
display: flex;
align-items: center;
}
.media-library-widget-modal .ui-dialog-buttonpane .form-actions {
flex: 1;
}
@media screen and (max-width: 600px) {
.media-library-view .form-actions {
flex-basis: 100%;
}
}
/* @todo Remove in https://www.drupal.org/project/drupal/issues/3064914 */
.views-live-preview .media-library-view div.views-row + div.views-row {
margin-top: 0;
}
......@@ -12,9 +12,10 @@
*/
Drupal.behaviors.MediaLibrarySelectAll = {
attach(context) {
const $view = $('.js-media-library-view', context).once(
'media-library-select-all',
);
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',
......
......@@ -8,7 +8,7 @@
(function ($, Drupal) {
Drupal.behaviors.MediaLibrarySelectAll = {
attach: function attach(context) {
var $view = $('.js-media-library-view', context).once('media-library-select-all');
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;
......
name: 'Media Library'
type: module
description: 'Enhances the media list with additional features to more easily find and use existing media items.'
package: Core (Experimental)
package: Core
version: VERSION
core: 8.x
dependencies:
......
style:
version: VERSION
css:
component:
css/media_library.module.css: {}
theme:
css/media_library.theme.css: {}
click_to_select:
version: VERSION
js:
......@@ -21,7 +13,6 @@ view:
dependencies:
- core/drupal.announce
- core/drupal.checkbox
- media_library/style
- media_library/click_to_select
widget:
......@@ -31,7 +22,6 @@ widget:
dependencies:
- core/drupal.dialog.ajax
- core/jquery.once
- media_library/style
- core/sortable
ui:
......
......@@ -24,7 +24,6 @@
use Drupal\media_library\Form\FileUploadForm;
use Drupal\media_library\Form\OEmbedForm;
use Drupal\media_library\MediaLibraryState;
use Drupal\views\Form\ViewsForm;
use Drupal\views\Plugin\views\cache\CachePluginBase;
use Drupal\views\ViewExecutable;
use Drupal\Core\StringTranslation\TranslatableMarkup;
......@@ -60,11 +59,21 @@ function media_library_help($route_name, RouteMatchInterface $route_match) {
$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('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.') . '</li>';
$output .= '<li>' . t('In the grid-style interface, which fields 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.', [
$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, which fields are displayed can be customized by configuring the "Media library" form mode for each of your <a href=":media-types">media types</a>.', [
$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>';
......@@ -91,9 +100,84 @@ function media_library_theme() {
'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().
*/
......@@ -143,23 +227,30 @@ function media_library_preprocess_media(&$variables) {
$variables['url'] = $media->toUrl($rel, [
'language' => $media->language(),
]);
$variables['preview_attributes'] = new Attribute();
$variables['preview_attributes']->addClass('media-library-item__preview', 'js-media-library-item-preview');
$variables['metadata_attributes'] = new Attribute();
$variables['metadata_attributes']->addClass('media-library-item__attributes');
$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
// styling and JavaScript mouseover and click events.
// 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 media-library-item__click-to-select-trigger');
$variables['fields']['rendered_entity']->wrapper_attributes->addClass('js-click-to-select-trigger');
}
}
}
......@@ -193,41 +284,32 @@ function media_library_form_views_form_media_library_page_alter(array &$form, Fo
* Implements hook_form_alter().
*/
function media_library_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
$form_object = $form_state->getFormObject();
if ($form_object instanceof ViewsForm && strpos($form_object->getBaseFormId(), 'views_form_media_library') === 0) {
$form['#attributes']['class'][] = 'media-library-views-form';
if (isset($form['header'])) {
$form['header']['#attributes']['class'][] = 'media-library-views-form__header';
$form['header']['media_bulk_form']['#attributes']['class'][] = 'media-library-views-form__bulk_form';
}
}
// Add after build to fix media library views exposed filter's submit button.
// 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_object instanceof MediaTypeForm) {
if ($form_state->getFormObject() instanceof MediaTypeForm) {
$form['actions']['submit']['#submit'][] = '_media_library_media_type_form_submit';
}
}
/**
* After build callback for views form media library.
* 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 media library views exposed filter actions
// and replace with .media-library-view--form-actions.
//
// This prevents the views exposed filter's 'Apply filter' submit button from
// being moved into the dialog's buttons.
// 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]);
}
$form['actions']['#attributes']['class'][] = 'media-library-view--form-actions';
return $form;
}
......
......@@ -18,9 +18,8 @@
* @ingroup ajax
*
* @internal
* Media Library is an experimental module and its internal code may be
* subject to change in minor releases. External code should not instantiate
* or extend this class.
* 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 {
......
......@@ -9,8 +9,11 @@
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;
......@@ -21,13 +24,8 @@
/**
* Provides a base class for creating media items from within the media library.
*
* @internal
* Media Library is an experimental module and its internal code may be
* subject to change in minor releases. External code should not instantiate
* or extend this class.
*/
abstract class AddFormBase extends FormBase {
abstract class AddFormBase extends FormBase implements BaseFormIdInterface, TrustedCallbackInterface {
/**
* The entity type manager.
......@@ -99,7 +97,7 @@ public static function create(ContainerInterface $container) {
/**
* {@inheritdoc}
*/
public function getFormId() {
public function getBaseFormId() {
return 'media_library_add_form';
}
......@@ -137,9 +135,8 @@ protected function getMediaType(FormStateInterface $form_state) {
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" class="media-library-add-form-wrapper">';
$form['#prefix'] = '<div id="media-library-add-form-wrapper">';
$form['#suffix'] = '</div>';
$form['#attached']['library'][] = 'media_library/style';
// 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
......@@ -157,27 +154,37 @@ public function buildForm(array $form, FormStateInterface $form_state) {
];
$form['#attributes']['class'] = [
'media-library-add-form',
'js-media-library-add-form',
];
$added_media = $this->getAddedMediaItems($form_state);
if (empty($added_media)) {
$form['#attributes']['class'][] = 'media-library-add-form--without-input';
$form = $this->buildInputElement($form, $form_state);
}
else {
$form['#attributes']['class'][] = 'media-library-add-form--with-input';
$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'] = [
'#type' => 'container',
'#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',
'media-library-add-form__added-media',
],
'aria-label' => $this->t('Added media items'),
'role' => 'list',
// 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
......@@ -186,18 +193,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
'tabindex' => '-1',
],
];
$form['media']['description'] = [
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $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.'),
'#attributes' => [
'class' => [
'media-library-add-form__description',
],
],
];
foreach ($added_media as $delta => $media) {
$form['media'][$delta] = $this->buildEntityFormElement($media, $form, $form_state, $delta);
}
......@@ -267,13 +262,8 @@ protected function buildEntityFormElement(MediaInterface $media, array $form, Fo
$id_suffix = $parents ? '-' . implode('-', $parents) : '';
$element = [
'#type' => 'container',
'#attributes' => [
'class' => [
'media-library-add-form__media',
],
'#wrapper_attributes' => [
'aria-label' => $media->getName(),
'role' => 'listitem',
// 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
......@@ -288,20 +278,10 @@ protected function buildEntityFormElement(MediaInterface $media, array $form, Fo
'preview' => [
'#type' => 'container',
'#weight' => 10,