Commit 4e88aed4 authored by webchick's avatar webchick

Issue #2938116 by samuel.mortenson, sjerdo, jefuri, chr.fritsch, yogeshmpawar,...

Issue #2938116 by samuel.mortenson, sjerdo, jefuri, chr.fritsch, yogeshmpawar, starshaped, phenaproxima, jibran, webchick, seanB, Berdir, lauriii, xjm, tstoeckler, dawehner, benjifisher, marcoscano, bdimaggio, martin107, mtodor, slashrsm, Gábor Hojtsy, ckrina, yoroy: Allow media to be uploaded with the Media Library field widget

(cherry picked from commit 53d521a3)
parent 33511d35
langcode: en
status: true
dependencies:
module:
- media
id: media.media_library
label: 'Media library'
targetEntityType: media
cache: true
langcode: en
status: true
dependencies:
config:
- core.entity_form_mode.media.media_library
- field.field.media.audio.field_media_audio_file
- media.type.audio
id: media.audio.media_library
targetEntityType: media
bundle: audio
mode: media_library
content:
name:
type: string_textfield
weight: 0
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
hidden:
created: true
field_media_audio_file: true
path: true
status: true
uid: true
langcode: en
status: true
dependencies:
config:
- core.entity_form_mode.media.media_library
- field.field.media.file.field_media_file
- media.type.file
id: media.file.media_library
targetEntityType: media
bundle: file
mode: media_library
content:
name:
type: string_textfield
weight: 0
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
hidden:
created: true
field_media_file: true
path: true
status: true
uid: true
langcode: en
status: true
dependencies:
config:
- core.entity_form_mode.media.media_library
- field.field.media.image.field_media_image
- image.style.thumbnail
- media.type.image
module:
- image
id: media.image.media_library
targetEntityType: media
bundle: image
mode: media_library
content:
field_media_image:
type: image_image
weight: 1
region: content
settings:
progress_indicator: throbber
preview_image_style: thumbnail
third_party_settings: { }
name:
type: string_textfield
weight: 0
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
hidden:
created: true
path: true
status: true
uid: true
langcode: en
status: true
dependencies:
config:
- core.entity_form_mode.media.media_library
- field.field.media.video.field_media_video_file
- media.type.video
id: media.video.media_library
targetEntityType: media
bundle: video
mode: media_library
content:
name:
type: string_textfield
weight: 0
region: content
settings:
size: 60
placeholder: ''
third_party_settings: { }
hidden:
created: true
field_media_video_file: true
path: true
status: true
uid: true
......@@ -198,6 +198,44 @@
border-color: #40b6ff;
}
/* Style the wrappers around new media and files */
.media-library-upload__media,
.media-library-upload__file {
display: flex;
flex-wrap: wrap;
padding: 20px 0 20px 0;
}
.media-library-upload__file {
align-items: center;
}
.media-library-upload__file-label {
margin-right: 10px;
}
/* @todo Remove in https://www.drupal.org/project/drupal/issues/2987921 */
.media-library-upload__source-field .file,
.media-library-upload__source-field .button,
.media-library-upload__source-field .image-preview,
.media-library-upload__source-field .form-type-managed-file > label,
.media-library-upload__source-field .file-size {
display: none;
}
.media-library-upload__media-preview {
margin-right: 20px;
width: 220px;
background: #ebebeb;
display: flex;
align-items: center;
justify-content: center;
}
.media-library-upload__media-preview img {
display: block;
}
/* @todo Remove or re-work in https://www.drupal.org/node/2985168 */
.media-library-widget .media-library-item__name a,
.media-library-view.view-display-id-widget .media-library-item__name a {
......
......@@ -6,11 +6,13 @@
*/
use Drupal\Component\Utility\UrlHelper;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\views\Form\ViewsForm;
use Drupal\views\Plugin\views\cache\CachePluginBase;
use Drupal\views\Plugin\views\query\QueryPluginBase;
......@@ -42,6 +44,36 @@ function media_library_theme() {
];
}
/**
* Implements hook_preprocess_view().
*
* Adds a link to add media above the view.
*/
function media_library_preprocess_views_view(&$variables) {
$view = $variables['view'];
if ($view->id() === 'media_library' && $view->current_display === 'widget') {
$url = Url::fromRoute('media_library.upload');
if ($url->access()) {
$url->setOption('query', \Drupal::request()->query->all());
$variables['header']['add_media'] = [
'#type' => 'link',
'#title' => t('Add media'),
'#url' => $url,
'#attributes' => [
'class' => ['button', 'button-action', 'button--primary', 'use-ajax'],
'data-dialog-type' => 'modal',
'data-dialog-options' => Json::encode([
'dialogClass' => 'media-library-widget-modal',
'height' => '75%',
'width' => '75%',
'title' => t('Add media'),
]),
],
];
}
}
}
/**
* Implements hook_views_post_render().
*/
......
media_library.upload:
path: '/admin/content/media-widget-upload'
defaults:
_form: '\Drupal\media_library\Form\MediaLibraryUploadForm'
requirements:
_custom_access: '\Drupal\media_library\Form\MediaLibraryUploadForm::access'
<?php
namespace Drupal\media_library\Form;
use Drupal\Core\Access\AccessResultAllowed;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\CloseDialogCommand;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\file\FileInterface;
use Drupal\file\Plugin\Field\FieldType\FileFieldItemList;
use Drupal\file\Plugin\Field\FieldType\FileItem;
use Drupal\media\MediaInterface;
use Drupal\media\MediaTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
/**
* Creates a form to create media entities from uploaded files.
*
* @internal
*/
class MediaLibraryUploadForm extends FormBase {
/**
* The element info manager.
*
* @var \Drupal\Core\Render\ElementInfoManagerInterface
*/
protected $elementInfo;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Media types the current user has access to.
*
* @var \Drupal\media\MediaTypeInterface[]
*/
protected $types;
/**
* The media being processed.
*
* @var \Drupal\media\MediaInterface[]
*/
protected $media = [];
/**
* The files waiting for type selection.
*
* @var \Drupal\file\FileInterface[]
*/
protected $files = [];
/**
* Indicates whether the 'medium' image style exists.
*
* @var bool
*/
protected $mediumStyleExists = FALSE;
/**
* Constructs a new MediaLibraryUploadForm.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
* The element info manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ElementInfoManagerInterface $element_info) {
$this->entityTypeManager = $entity_type_manager;
$this->elementInfo = $element_info;
$this->mediumStyleExists = !empty($entity_type_manager->getStorage('image_style')->load('medium'));
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('element_info')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'media_library_upload_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form['#prefix'] = '<div id="media-library-upload-wrapper">';
$form['#suffix'] = '</div>';
$form['#attached']['library'][] = 'media_library/style';
$form['#attributes']['class'][] = 'media-library-upload';
if (empty($this->media) && empty($this->files)) {
$process = (array) $this->elementInfo->getInfoProperty('managed_file', '#process', []);
$upload_validators = $this->mergeUploadValidators($this->getTypes());
$form['upload'] = [
'#type' => 'managed_file',
'#title' => $this->t('Upload'),
// @todo Move validation in https://www.drupal.org/node/2988215
'#process' => array_merge(['::validateUploadElement'], $process, ['::processUploadElement']),
'#upload_validators' => $upload_validators,
];
$form['upload_help'] = [
'#theme' => 'file_upload_help',
'#description' => $this->t('Upload files here to add new media.'),
'#upload_validators' => $upload_validators,
];
$remaining = $this->getRequest()->query->get('media_library_remaining');
if ($remaining) {
$form['upload']['#multiple'] = $remaining > 1;
$form['upload']['#cardinality'] = $form['upload_help']['#cardinality'] = (int) $remaining;
}
}
else {
$form['media'] = [
'#type' => 'container',
];
foreach ($this->media as $i => $media) {
$source_field = $media->getSource()
->getSourceFieldDefinition($media->bundle->entity)
->getName();
$element = [
'#type' => 'container',
'#attributes' => [
'class' => [
'media-library-upload__media',
],
],
'preview' => [
'#type' => 'container',
'#attributes' => [
'class' => [
'media-library-upload__media-preview',
],
],
],
'fields' => [
'#type' => 'container',
'#attributes' => [
'class' => [
'media-library-upload__media-fields',
],
],
// Parents is set here as it is used in the form display.
'#parents' => ['media', $i, 'fields'],
],
];
// @todo Make this configurable in https://www.drupal.org/node/2988223
if ($this->mediumStyleExists && $thumbnail_uri = $media->getSource()->getMetadata($media, 'thumbnail_uri')) {
$element['preview']['thumbnail'] = [
'#theme' => 'image_style',
'#style_name' => 'medium',
'#uri' => $thumbnail_uri,
];
}
EntityFormDisplay::collectRenderDisplay($media, 'media_library')
->buildForm($media, $element['fields'], $form_state);
// We hide certain elements in the image widget with CSS.
if (isset($element['fields'][$source_field])) {
$element['fields'][$source_field]['#attributes']['class'][] = 'media-library-upload__source-field';
}
if (isset($element['fields']['revision_log_message'])) {
$element['fields']['revision_log_message']['#access'] = FALSE;
}
$form['media'][$i] = $element;
}
$form['files'] = [
'#type' => 'container',
];
foreach ($this->files as $i => $file) {
$types = $this->filterTypesThatAcceptFile($file, $this->getTypes());
$form['files'][$i] = [
'#type' => 'container',
'#attributes' => [
'class' => [
'media-library-upload__file',
],
],
'help' => [
'#markup' => '<strong class="media-library-upload__file-label">' . $this->t('Select a media type for %filename:', [
'%filename' => $file->getFilename(),
]) . '</strong>',
],
];
foreach ($types as $type) {
$form['files'][$i][$type->id()] = [
'#type' => 'submit',
'#media_library_index' => $i,
'#media_library_type' => $type->id(),
'#value' => $type->label(),
'#submit' => ['::selectType'],
'#ajax' => [
'callback' => '::updateFormCallback',
'wrapper' => 'media-library-upload-wrapper',
],
'#limit_validation_errors' => [['files', $i, $type->id()]],
];
}
}
$form['actions'] = [
'#type' => 'actions',
];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
'#ajax' => [
'callback' => '::updateWidget',
'wrapper' => 'media-library-upload-wrapper',
],
];
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
if (count($this->files)) {
$form_state->setError($form['files'], $this->t('Please select a media type for all files.'));
}
foreach ($this->media as $i => $media) {
$form_display = EntityFormDisplay::collectRenderDisplay($media, 'media_library');
$form_display->extractFormValues($media, $form['media'][$i]['fields'], $form_state);
$form_display->validateFormValues($media, $form['media'][$i]['fields'], $form_state);
}
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
foreach ($this->media as $i => $media) {
EntityFormDisplay::collectRenderDisplay($media, 'media_library')
->extractFormValues($media, $form['media'][$i]['fields'], $form_state);
$source_field = $media->getSource()->getSourceFieldDefinition($media->bundle->entity)->getName();
/** @var \Drupal\file\FileInterface $file */
$file = $media->get($source_field)->entity;
$file->setPermanent();
$file->save();
$media->save();
}
}
/**
* AJAX callback to select a media type for a file.
*
* @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.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* If the triggering element is missing required properties.
*/
public function selectType(array &$form, FormStateInterface $form_state) {
$element = $form_state->getTriggeringElement();
if (!isset($element['#media_library_index']) || !isset($element['#media_library_type'])) {
throw new BadRequestHttpException('The "#media_library_index" and "#media_library_type" properties on the triggering element are required for type selection.');
}
$i = $element['#media_library_index'];
$type = $element['#media_library_type'];
$this->media[] = $this->createMediaEntity($this->files[$i], $this->getTypes()[$type]);
unset($this->files[$i]);
$form_state->setRebuild();
}
/**
* AJAX callback to update the field widget.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* A command to send the selection to the current field widget.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* If the "media_library_widget_id" query parameter is not present.
*/
public function updateWidget(array &$form, FormStateInterface $form_state) {
if ($form_state->getErrors()) {
return $form;
}
$widget_id = $this->getRequest()->query->get('media_library_widget_id');
if (!$widget_id || !is_string($widget_id)) {
throw new BadRequestHttpException('The "media_library_widget_id" query parameter is required and must be a string.');
}
$mids = array_map(function (MediaInterface $media) {
return $media->id();
}, $this->media);
// Pass the selection to the field widget based on the current widget ID.
return (new AjaxResponse())
->addCommand(new InvokeCommand("[data-media-library-widget-value=\"$widget_id\"]", 'val', [implode(',', $mids)]))
->addCommand(new InvokeCommand("[data-media-library-widget-update=\"$widget_id\"]", 'trigger', ['mousedown']))
->addCommand(new CloseDialogCommand());
}
/**
* 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'];
$element['upload_button']['#ajax'] = [
'callback' => '::updateFormCallback',
'wrapper' => 'media-library-upload-wrapper',
];
return $element;
}
/**
* 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->getErrors()) {
$element['#value'] = [];
}
$values = $form_state->getValue('upload', []);
if (count($values['fids']) > $element['#cardinality']) {
$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;
}
/**
* 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) {
$fids = $form_state->getValue('upload', []);
$files = $this->entityTypeManager->getStorage('file')->loadMultiple($fids);
/** @var \Drupal\file\FileInterface $file */
foreach ($files as $file) {
$types = $this->filterTypesThatAcceptFile($file, $this->getTypes());
if (!empty($types)) {
if (count($types) === 1) {
$this->media[] = $this->createMediaEntity($file, reset($types));
}
else {
$this->files[] = $file;
}
}
}
$form_state->setRebuild();
}
/**
* Creates a new, unsaved media entity.
*
* @param \Drupal\file\FileInterface $file
* A file for the media source field.
* @param \Drupal\media\MediaTypeInterface $type
* A media type.
*
* @return \Drupal\media\MediaInterface
* An unsaved media entity.
*
* @throws \Exception
* If a file operation failed when moving the upload.
*/
protected function createMediaEntity(FileInterface $file, MediaTypeInterface $type) {
$media = $this->entityTypeManager->getStorage('media')->create([
'bundle' => $type->id(),
'name' => $file->getFilename(),
]);
$source_field = $type->getSource()->getSourceFieldDefinition($type)->getName();
$location = $this->getUploadLocationForType($media->bundle->entity);
if (!file_prepare_directory($location, FILE_CREATE_DIRECTORY)) {
throw new \Exception("The destination directory '$location' is not writable");
}
$file = file_move($file, $location);
if (!$file) {
throw new \Exception("Unable to move file to '$location'");
}
$media->set($source_field, $file->id());
return $media;
}
/**
* AJAX callback for refreshing the entire form.
*
* @param array $form
* The form render array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The form render array.
*/
public function updateFormCallback(array &$form, FormStateInterface $form_state) {
return $form;
}
/**
* Access callback to check that the user can create file based media.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access() {
return AccessResultAllowed::allowedIf(count($this->getTypes()))->mergeCacheMaxAge(0);
}
/**
* Returns media types which use files that the current user can create.
*
* @todo Move in https://www.drupal.org/node/2987924