diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php
new file mode 100644
index 0000000000..7e36a33dcd
--- /dev/null
+++ b/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php
@@ -0,0 +1,51 @@
+ '70%',
+ ];
+
+ /**
+ * OpenDialogUrlCommand constructor.
+ *
+ * @param \Drupal\Core\Url|string $url
+ * The URL to open in the modal dialog.
+ * @param array $dialog_options
+ * (optional) Additional client-side options for the modal dialog.
+ */
+ public function __construct($url, array $dialog_options = []) {
+ $this->url = $url instanceof Url ? $url->toString() : $url;
+ if ($dialog_options) {
+ $this->dialogOptions = $dialog_options;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ return [
+ 'command' => 'openDialogUrl',
+ 'url' => $this->url,
+ 'dialogOptions' => $this->dialogOptions,
+ ];
+ }
+
+}
diff --git a/core/misc/dialog/dialog.ajax.es6.js b/core/misc/dialog/dialog.ajax.es6.js
index 798673a1d8..6c8b44a809 100644
--- a/core/misc/dialog/dialog.ajax.es6.js
+++ b/core/misc/dialog/dialog.ajax.es6.js
@@ -95,6 +95,27 @@
};
/**
+ * Command to open an arbitrary URL in a modal dialog.
+ *
+ * @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.openDialogUrl = function (ajax, response, status) {
+ new Drupal.Ajax(null, null, {
+ url: response.url,
+ dialogType: 'modal',
+ dialog: response.dialogOptions,
+ progress: {
+ type: 'throbber'
+ }
+ }).execute();
+ };
+
+ /**
* Command to open a dialog.
*
* @param {Drupal.Ajax} ajax
diff --git a/core/misc/dialog/dialog.ajax.js b/core/misc/dialog/dialog.ajax.js
index 8277df3e51..15e11f39c4 100644
--- a/core/misc/dialog/dialog.ajax.js
+++ b/core/misc/dialog/dialog.ajax.js
@@ -59,6 +59,17 @@
}
};
+ Drupal.AjaxCommands.prototype.openDialogUrl = function (ajax, response, status) {
+ new Drupal.Ajax(null, null, {
+ url: response.url,
+ dialogType: 'modal',
+ dialog: response.dialogOptions,
+ progress: {
+ type: 'throbber'
+ }
+ }).execute();
+ };
+
Drupal.AjaxCommands.prototype.openDialog = function (ajax, response, status) {
if (!response.selector) {
return false;
diff --git a/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml b/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml
new file mode 100644
index 0000000000..e2ed5cefbd
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_form_display.media.file.add_file.yml
@@ -0,0 +1,34 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - core.entity_form_mode.media.add_file
+ - field.field.media.file.field_media_file
+ - media.type.file
+ module:
+ - file
+id: media.file.add_file
+targetEntityType: media
+bundle: file
+mode: add_file
+content:
+ field_media_file:
+ settings:
+ progress_indicator: throbber
+ third_party_settings: { }
+ type: file_generic
+ weight: 1
+ 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
diff --git a/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml b/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml
new file mode 100644
index 0000000000..75be602501
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_form_display.media.image.add_file.yml
@@ -0,0 +1,36 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - core.entity_form_mode.media.add_file
+ - field.field.media.image.field_media_image
+ - image.style.thumbnail
+ - media.type.image
+ module:
+ - image
+id: media.image.add_file
+targetEntityType: media
+bundle: image
+mode: add_file
+content:
+ field_media_image:
+ settings:
+ progress_indicator: throbber
+ preview_image_style: thumbnail
+ third_party_settings: { }
+ type: image_image
+ weight: 1
+ 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
diff --git a/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml b/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml
new file mode 100644
index 0000000000..ff7eccdeb5
--- /dev/null
+++ b/core/modules/media/config/install/core.entity_form_mode.media.add_file.yml
@@ -0,0 +1,9 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - media
+id: media.add_file
+label: 'File Upload'
+targetEntityType: media
+cache: true
diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml
index dad518985a..1176376da8 100644
--- a/core/modules/media/config/schema/media.schema.yml
+++ b/core/modules/media/config/schema/media.schema.yml
@@ -40,6 +40,14 @@ field.formatter.settings.media_thumbnail:
type: field.formatter.settings.image
label: 'Media thumbnail field display format settings'
+field.widget.settings.media_file:
+ type: mapping
+ label: 'File widget settings'
+ mapping:
+ open:
+ type: boolean
+ label: 'Open by default'
+
media.source.*:
type: mapping
label: 'Media source settings'
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index dc2d898e3c..f89ae4a730 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -6,12 +6,15 @@
*/
use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\field\FieldConfigInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
+use Drupal\file\FileInterface;
+use Drupal\media\Form\AddFileForm;
/**
* Implements hook_help().
@@ -96,3 +99,39 @@ function template_preprocess_media(array &$variables) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
+
+/**
+ * Validate uploaded file for media field.
+ *
+ * @param \Drupal\file\FileInterface $file
+ * File that should be validated.
+ *
+ * @return array
+ * Result of validation.
+ */
+function media_validate_file_upload(FileInterface $file) {
+ $media_types = \Drupal::service('media.type_guesser.file')->getTypesForFile($file);
+
+ if (empty($media_types)) {
+ return [
+ t('%filename could not be matched to any media types.', [
+ '%filename' => $file->getFilename(),
+ ]),
+ ];
+ }
+ return [];
+}
+
+/**
+ * Implements hook_field_widget_WIDGET_TYPE_form_alter().
+ */
+function media_field_widget_file_generic_form_alter(array &$element, FormStateInterface $form_state, array $context) {
+ AddFileForm::alterFileWidget($element, $form_state);
+}
+
+/**
+ * Implements hook_field_widget_WIDGET_TYPE_form_alter().
+ */
+function media_field_widget_image_image_form_alter(array &$element, FormStateInterface $form_state, array $context) {
+ AddFileForm::alterFileWidget($element, $form_state);
+}
diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml
index 9fbadeff29..fb7a582f4c 100644
--- a/core/modules/media/media.routing.yml
+++ b/core/modules/media/media.routing.yml
@@ -19,3 +19,16 @@ entity.media.revision:
requirements:
_access_media_revision: 'view'
media: \d+
+
+entity.media.add_file:
+ path: '/media/add-file/{media_type}/{file}/{selector}'
+ defaults:
+ _entity_form: 'media.add_file'
+ options:
+ parameters:
+ media_type:
+ type: 'entity:media_type'
+ file:
+ type: 'entity:file'
+ requirements:
+ _entity_create_access: 'media'
diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml
index f22f90a124..7896a10ce1 100644
--- a/core/modules/media/media.services.yml
+++ b/core/modules/media/media.services.yml
@@ -8,3 +8,8 @@ services:
arguments: ['@entity_type.manager']
tags:
- { name: access_check, applies_to: _access_media_revision }
+
+ media.type_guesser.file:
+ class: Drupal\media\FileGuesser
+ # TODO: Add a cache backend.
+ arguments: ['@entity_type.manager']
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index 49f7283f7e..dea100180c 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -38,6 +38,7 @@
* "add" = "Drupal\media\MediaForm",
* "edit" = "Drupal\media\MediaForm",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
+ * "add_file" = "Drupal\media\Form\AddFileForm",
* },
* "translation" = "Drupal\content_translation\ContentTranslationHandler",
* "views_data" = "Drupal\media\MediaViewsData",
diff --git a/core/modules/media/src/FileGuesser.php b/core/modules/media/src/FileGuesser.php
new file mode 100644
index 0000000000..1f69ee7f24
--- /dev/null
+++ b/core/modules/media/src/FileGuesser.php
@@ -0,0 +1,152 @@
+entityTypeManager = $entity_type_manager;
+ $this->cacheBackend = $cache_backend;
+ $this->useCaches = isset($cache_backend);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function guessForFile($file, array $limit = []) {
+ $media_types = $this->getTypesForFile($file, $limit);
+ return key($media_types);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function guessForExtension($extension, array $limit = []) {
+ $media_types = $this->getTypesByExtension($extension, $limit);
+ return key($media_types);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypesForFile($file, array $limit = []) {
+ if (is_numeric($file)) {
+ $file = $this->entityTypeManager->getStorage('file')->load($file);
+ }
+
+ if ($file instanceof FileInterface) {
+ $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
+
+ return $this->getTypesByExtension($extension, $limit);
+ }
+ else {
+ throw new \InvalidArgumentException('Expected file ID or entity');
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypesByExtension($extension, array $limit = []) {
+ $cache = $this->cacheGet($extension);
+ if ($cache) {
+ return $cache->data;
+ }
+
+ $media_types = $this->getAllowedMediaTypes();
+ if ($limit) {
+ $media_types = array_intersect_key($media_types, array_flip($limit));
+ }
+
+ $filter = function (MediaTypeInterface $media_type) {
+ $item_class = $media_type
+ ->getSource()
+ ->getSourceFieldDefinition($media_type)
+ ->getItemDefinition()
+ ->getClass();
+
+ return is_a($item_class, FileItem::class, TRUE);
+ };
+ $media_types = array_filter($media_types, $filter);
+
+ $extension = Unicode::strtolower($extension);
+
+ $matches = [];
+ /** @var \Drupal\media\MediaTypeInterface $media_type */
+ foreach ($media_types as $id => $media_type) {
+ $extensions = $media_type
+ ->getSource()
+ ->getSourceFieldDefinition($media_type)
+ ->getSetting('file_extensions');
+
+ $allowed_extensions = preg_split('/\s+/', Unicode::strtolower($extensions));
+
+ if (in_array($extension, $allowed_extensions)) {
+ $matches[$id] = $media_type->label();
+ }
+ }
+ $this->cacheSet($extension, $matches);
+
+ return $matches;
+ }
+
+ /**
+ * Returns all media types that can be created by the current user.
+ *
+ * @return \Drupal\media\MediaTypeInterface[]
+ * The media types.
+ */
+ protected function getAllowedMediaTypes() {
+ if (isset($this->allowedTypes)) {
+ return $this->allowedTypes;
+ }
+
+ $storage = $this->entityTypeManager->getStorage('media_type');
+
+ $media_types = array_filter(
+ $storage->getQuery()->execute(),
+ [
+ $this->entityTypeManager->getAccessControlHandler('media'),
+ 'createAccess',
+ ]
+ );
+ $this->allowedTypes = $storage->loadMultiple($media_types);
+
+ return $this->allowedTypes;
+ }
+
+}
diff --git a/core/modules/media/src/FileGuesserInterface.php b/core/modules/media/src/FileGuesserInterface.php
new file mode 100644
index 0000000000..fa97260506
--- /dev/null
+++ b/core/modules/media/src/FileGuesserInterface.php
@@ -0,0 +1,69 @@
+fileGuesser = $file_guesser;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity.manager'),
+ $container->get('media.type_guesser.file'),
+ $container->get('entity_type.bundle.info'),
+ $container->get('datetime.time')
+ );
+ }
+
+ /**
+ * AJAX callback associated with the 'media type' input selector.
+ *
+ * @param array $form
+ * The form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return array
+ * The forms 'fields' array.
+ */
+ public static function onTypeSelect(array &$form, FormStateInterface $form_state) {
+ return $form['fields'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function init(FormStateInterface $form_state) {
+ if ($form_state->hasValue('bundle')) {
+ $entity = $this->entityTypeManager
+ ->getStorage('media')
+ ->create([
+ 'bundle' => $form_state->getValue('bundle'),
+ ]);
+
+ $this->setEntity($entity);
+ }
+ return parent::init($form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function prepareEntity() {
+ parent::prepareEntity();
+
+ /** @var \Drupal\media\MediaInterface $entity */
+ $entity = $this->getEntity();
+
+ $source_field = $entity->getSource()
+ ->getSourceFieldDefinition($entity->bundle->entity)
+ ->getName();
+
+ /** @var \Drupal\file\FileInterface $file */
+ $file = $this->getRouteMatch()->getParameter('file');
+ $entity->set($source_field, $file);
+
+ // Move the file to the location specified by the source field.
+ /** @var \Drupal\file\Plugin\Field\FieldType\FileItem $item */
+ $item = $entity->get($source_field)->first();
+ $destination = $item->getUploadLocation();
+ file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
+ $destination .= '/' . $file->getFilename();
+ if (!file_exists($destination)) {
+ // TODO: Use an injected file_system service for this.
+ $destination = file_unmanaged_move($file->getFileUri(), $destination);
+ }
+ $file->setFileUri($destination);
+ // TODO: The file is saved on every bundle change. We might not want that.
+ $file->save();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $entity = parent::validateForm($form, $form_state);
+
+ // If the bundle has changed, reinitialize the entity form. This will call
+ // init(), which will replace the current entity with a new one of the
+ // selected type.
+ $chosen_bundle = $form_state->getValue('bundle');
+ if ($chosen_bundle && $chosen_bundle !== $this->getEntity()->bundle()) {
+ $storage = &$form_state->getStorage();
+ unset($storage['entity_form_initialized']);
+ }
+
+ return $entity;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form['fields'] = parent::form($form, $form_state);
+ $form['fields']['#prefix'] = '
';
+ $form['fields']['#suffix'] = '
';
+
+ /** @var \Drupal\media\MediaInterface $entity */
+ $entity = $this->getEntity();
+ $source_field = $entity->getSource()
+ ->getSourceFieldDefinition($entity->bundle->entity)
+ ->getName();
+ /** @var \Drupal\file\FileInterface $file */
+ $file = $entity->get($source_field)->entity;
+ $form['message'] = [
+ '#type' => 'markup',
+ '#markup' => $this->t('Choose a media type for %filename', ['%filename' => $file->getFilename()]),
+ '#weight' => -110,
+ ];
+
+ $form['bundle'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Media Type'),
+ '#required' => TRUE,
+ '#default_value' => $this->getEntity()->bundle(),
+ '#weight' => -100,
+ '#ajax' => [
+ 'wrapper' => 'entity-fields',
+ 'callback' => [static::class, 'onTypeSelect'],
+ ],
+ '#limit_validation_errors' => [
+ ['bundle'],
+ ],
+ '#submit' => [],
+ ];
+
+ // TODO: Currently the types are fetched based on the field settings, but
+ // since every type can have it's own upload validators, we need to account
+ // for that as well. One media type might support PDF files, while another
+ // media type doesn't.
+ $query = $this->getRequest()->query;
+ $entity_type = $query->get('entity_type');
+ $bundle = $query->get('bundle');
+ $field_name = $query->get('field_name');
+ if ($entity_type && $bundle && $field_name) {
+ $field_definitions = $this->entityManager->getFieldDefinitions($entity_type, $bundle);
+
+ if (!isset($field_definitions[$field_name])) {
+ return $form;
+ }
+
+ $allowed_types = MediaType::loadMultiple($field_definitions[$field_name]->getSetting('handler_settings')['target_bundles']);
+ foreach ($allowed_types as $media_type) {
+ /** @var \Drupal\media\Entity\MediaType $media_type */
+ $source = $media_type->getSource();
+ if ($source->getPluginId() === 'file') {
+ $form['bundle']['#options'][$media_type->id()] = $media_type->label();
+ }
+ }
+ }
+
+ // Hide revision log message field.
+ $form['fields']['revision_log_message']['#access'] = FALSE;
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions = parent::actions($form, $form_state);
+
+ $actions['submit']['#ajax'] = [
+ 'callback' => '::onSave',
+ ];
+
+ return $actions;
+ }
+
+ /**
+ * The 'onSave' action callback.
+ *
+ * @param array $form
+ * The form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * The AJAX response.
+ */
+ public function onSave(array &$form, FormStateInterface $form_state) {
+ $response = new AjaxResponse();
+
+ if ($form_state->getErrors()) {
+ return $response->addCommand(
+ new BeforeCommand('#entity-fields', ['#type' => 'status_messages'])
+ );
+ }
+
+ $route_match = $this->getRouteMatch();
+ $parameters = $route_match->getRawParameters()->all();
+ $query = $this->getRequest()->query;
+
+ $media_items = $query->get('media', []);
+ array_push($media_items, $this->getEntity()->id());
+
+ if ($query->has('files')) {
+ $query = [
+ 'files' => $query->get('files'),
+ 'media' => $media_items,
+ ];
+ $next_file_id = array_shift($query['files']);
+ $parameters['file'] = $next_file_id;
+ $parameters['media_type'] = $this->fileGuesser->guessForFile($next_file_id);
+
+ $url = Url::fromRoute($route_match->getRouteName(), $parameters, [
+ 'query' => $query,
+ ]);
+ $response->addCommand(new OpenDialogUrlCommand($url));
+ }
+ else {
+ $response
+ ->addCommand(new InvokeCommand(
+ 'input[data-drupal-selector="' . $parameters['selector'] . '"]',
+ 'val',
+ (array) implode(',', array_map('intval', $media_items))
+ ))
+ ->addCommand(new CloseModalDialogCommand());
+ }
+ return $response;
+ }
+
+ /**
+ * Alters the field widget by hiding the remove button.
+ *
+ * @param array $element
+ * The elements.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public static function alterFileWidget(array &$element, FormStateInterface $form_state) {
+ $self = static::class;
+
+ if ($form_state->getFormObject() instanceof $self) {
+ $element['#process'][] = [$self, 'hideRemoveButton'];
+ }
+ }
+
+ /**
+ * Hides the remove button.
+ *
+ * @param array $element
+ * An associative array containing the 'remove_button' render element.
+ *
+ * @return array
+ * An associated array containing the updated render array.
+ */
+ public static function hideRemoveButton(array $element) {
+ $element['remove_button']['#access'] = FALSE;
+ return $element;
+ }
+
+ /**
+ * Checks whether the revision form fields should be added to the form.
+ *
+ * @return bool
+ * TRUE if the form field should be added, FALSE otherwise.
+ */
+ protected function showRevisionUi() {
+ return FALSE;
+ }
+
+}
diff --git a/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
new file mode 100644
index 0000000000..33a779d09a
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
@@ -0,0 +1,269 @@
+managedFileInfo = $managed_file_info;
+ $this->renderer = $renderer;
+ }
+
+ /**
+ * {@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('element_info')->getInfo('managed_file'),
+ $container->get('renderer')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ $settings = [
+ 'open' => TRUE,
+ ];
+ return $settings + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function isApplicable(FieldDefinitionInterface $field_definition) {
+ if ($field_definition->getType() !== 'entity_reference') {
+ return FALSE;
+ }
+
+ if ($field_definition->getFieldStorageDefinition()->getSetting('target_type') !== 'media') {
+ return FALSE;
+ }
+
+ $allowed_types = MediaType::loadMultiple($field_definition->getSetting('handler_settings')['target_bundles']);
+ foreach ($allowed_types as $media_type) {
+ /** @var \Drupal\media\Entity\MediaType $media_type */
+ $source = $media_type->getSource();
+ if ($source->getPluginId() === 'file') {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ $field_definition = $items->getFieldDefinition();
+ $field_storage_definition = $field_definition->getFieldStorageDefinition();
+
+ $element = [
+ '#type' => 'details',
+ '#title' => $field_definition->getLabel(),
+ '#attributes' => [
+ 'title' => $field_definition->getLabel(),
+ ],
+ '#attached' => [
+ 'library' => [
+ 'core/drupal.dialog.ajax',
+ ],
+ ],
+ '#open' => $this->getSetting('open'),
+ ];
+
+ $cardinality = $field_storage_definition->getCardinality();
+
+ $upload_validators = [[]];
+ $allowed_types = MediaType::loadMultiple($field_definition->getSetting('handler_settings')['target_bundles']);
+ foreach ($allowed_types as $media_type) {
+ /** @var \Drupal\media\Entity\MediaType $media_type */
+ $source = $media_type->getSource();
+ if ($source->getPluginId() === 'file') {
+ $source_data_definition = FieldItemDataDefinition::create($source->getSourceFieldDefinition($media_type));
+ $file_item = new FileItem($source_data_definition);
+ $upload_validators[] = $file_item->getUploadValidators();
+ }
+ }
+ $upload_validators = array_merge(...$upload_validators);
+
+ $file_upload_help = [
+ '#theme' => 'file_upload_help',
+ '#upload_validators' => $upload_validators,
+ '#cardinality' => $cardinality,
+ ];
+
+ $element['file'] = [
+ '#type' => 'managed_file',
+ '#multiple' => $cardinality !== 1 ? TRUE : FALSE,
+ // Hide the file element if the field is at capacity.
+ '#access' => $items->isEmpty() || ($cardinality > 0 && count($items) < $cardinality),
+ '#upload_validators' => $upload_validators + [
+ 'media_validate_file_upload' => [],
+ ],
+ '#description' => $this->renderer->renderPlain($file_upload_help),
+ '#entity_type' => $field_definition->getTargetEntityTypeId(),
+ '#bundle' => $field_definition->getTargetBundle(),
+ '#field_name' => $field_definition->getName(),
+ ];
+
+ // Attach a AJAX callback using '#process' information extracted from the
+ // 'managed_file' element type.
+ $element['file']['#process'] = $this->managedFileInfo['#process'];
+ $element['file']['#process'][] = [static::class, 'processFileElement'];
+
+ $element['media_items']['#type'] = 'hidden';
+
+ return $element;
+ }
+
+ /**
+ * Adds this widget to the AJAX callback of the upload_button.
+ *
+ * @param array $element
+ * A render array containing the upload button.
+ *
+ * @return array
+ * An updated render array.
+ */
+ public static function processFileElement(array $element) {
+ $element['upload_button']['#ajax']['callback'][0] = static::class;
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
+ if (empty($values['media_items'])) {
+ return [];
+ }
+
+ return array_map(
+ function ($media_id) {
+ return [
+ 'target_id' => (int) $media_id,
+ ];
+ },
+ explode(',', $values['media_items'])
+ );
+ }
+
+ /**
+ * Ajax callback for media_file upload forms.
+ *
+ * Augments the ManagedFile::uploadAjaxCallback() with a OpenDialogUrlCommand
+ * and its associated data.
+ *
+ * @param array $form
+ * The form render array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The request.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * An updated AjaxResponse.
+ */
+ public static function uploadAjaxCallback(array &$form, FormStateInterface $form_state, Request $request) {
+ $response = ManagedFile::uploadAjaxCallback($form, $form_state, $request);
+
+ $trigger = $form_state->getTriggeringElement();
+ $complete_form = $form_state->getCompleteForm();
+ $widget = NestedArray::getValue($complete_form, array_slice($trigger['#array_parents'], 0, -2));
+
+ $files = $widget['file']['#value']['fids'];
+
+ if (!$files) {
+ return $response;
+ }
+
+ $parameters = [
+ 'file' => array_shift($files),
+ 'selector' => $widget['media_items']['#attributes']['data-drupal-selector'],
+ ];
+ $parameters['media_type'] = \Drupal::service('media.type_guesser.file')->guessForFile($parameters['file']);
+ $options = [
+ 'query' => [
+ 'files' => $files,
+ 'entity_type' => $widget['file']['#entity_type'],
+ 'bundle' => $widget['file']['#bundle'],
+ 'field_name' => $widget['file']['#field_name'],
+ ],
+ ];
+ $url = Url::fromRoute('entity.media.add_file', $parameters, $options);
+
+ return $response->addCommand(new OpenDialogUrlCommand($url));
+ }
+
+}
diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php
new file mode 100644
index 0000000000..04c8a9a80a
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php
@@ -0,0 +1,137 @@
+nodeType = $this->drupalCreateContentType();
+
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => 'field_media',
+ 'entity_type' => 'node',
+ 'type' => 'entity_reference',
+ 'settings' => [
+ 'target_type' => 'media',
+ ],
+ 'cardinality' => 3,
+ ]);
+ $field_storage->save();
+
+ FieldConfig::create([
+ 'label' => 'Media',
+ 'bundle' => $this->nodeType->id(),
+ 'field_storage' => $field_storage,
+ 'settings' => [
+ 'handler' => 'default:media',
+ 'handler_settings' => [
+ 'target_bundles' => NULL,
+ ],
+ ],
+ ])->save();
+
+ entity_get_form_display('node', $this->nodeType->id(), 'default')
+ ->setComponent('field_media', [
+ 'type' => 'media_file',
+ 'region' => 'content',
+ ])
+ ->save();
+ }
+
+ /**
+ * Tests various feature of the MediaFileWidget through the UI.
+ *
+ * By attaching it to a node and uploading a file and inspected various
+ * stages.
+ */
+ public function testWidget() {
+ $assert = $this->assertSession();
+
+ $account = $this->drupalCreateUser([
+ 'create media',
+ 'create ' . $this->nodeType->id() . ' content',
+ 'edit own ' . $this->nodeType->id() . ' content',
+ ]);
+ $this->drupalLogin($account);
+
+ $this->drupalGet('/node/add/' . $this->nodeType->id());
+ $wrapper = $assert->elementExists('css', 'details[open][title = "Media"]');
+ // Assert that multiple-cardinality media reference fields produce only
+ // a single multiple-value file upload element.
+ /** @var \Behat\Mink\Element\NodeElement[] $file_elements */
+ $file_elements = $wrapper->findAll('css', 'input[type = "file"]');
+ $this->assertCount(1, $file_elements);
+ $file_element = reset($file_elements);
+ $this->assertSame('files[field_media_file][]', $file_element->getAttribute('name'));
+ $this->assertTrue($file_element->hasAttribute('multiple'));
+
+ // Upload a file and assert that the media form shows up as expected.
+ $this->getSession()->getPage()->attachFileToField($file_element->getAttribute('name'), __DIR__ . '/../../fixtures/example_1.jpeg');
+ $this->waitUntilVisible('#drupal-modal', 30000);
+ $form_element = $assert->elementExists('css', '#drupal-modal form');
+ $assert->fieldValueEquals('Media type', 'image', $form_element);
+ $assert->fieldExists('Name', $form_element);
+ $fid = $assert->hiddenFieldExists('inner_form[field_media_image][0][fids]', $form_element)->getValue();
+ $this->assertNotEmpty($fid);
+ $this->assertTrue(is_numeric($fid));
+ $assert->buttonExists('Save', $form_element);
+
+ $buttons = $assert->elementExists('css', '#drupal-modal + .ui-dialog-buttonpane');
+ $save_button = $assert->buttonExists('Save', $buttons);
+ $save_button->press();
+
+ $errors_selector = '#drupal-modal form .messages--error';
+ $this->waitUntilVisible($errors_selector, 30000);
+ $assert->elementExists('named', ['content', 'Name field is required.'], $form_element);
+ $assert->elementExists('named', ['content', 'Alternative text field is required.'], $form_element);
+
+ $this->getSession()->executeScript('document.querySelector("' . $errors_selector . '").remove()');
+ $form_element->fillField('Name', 'Pastafazoul');
+ $save_button->press();
+ $this->waitUntilVisible($errors_selector, 30000);
+ $assert->elementNotExists('named', ['content', 'Name field is required.'], $form_element);
+ $assert->elementExists('named', ['content', 'Alternative text field is required.'], $form_element);
+
+ // Assert that the file element is hidden if the field is at capacity.
+ $node = $this->drupalCreateNode([
+ 'type' => $this->nodeType->id(),
+ 'uid' => $account->id(),
+ ]);
+ $media_type = $this->createMediaType()->id();
+ for ($i = 0; $i < 3; $i++) {
+ $media = Media::create([
+ 'bundle' => $media_type,
+ 'field_media_test' => $this->randomString(),
+ ]);
+ $media->save();
+ $node->field_media[$i] = $media;
+ }
+ $node->save();
+
+ $this->drupalGet('/node/' . $node->id() . '/edit');
+ $wrapper = $assert->elementExists('css', 'details[open][title = "Media"]');
+ $assert->elementNotExists('css', 'input[type = "file"]', $wrapper);
+ }
+
+}
diff --git a/core/modules/media/tests/src/Kernel/MediaFileWidgetTest.php b/core/modules/media/tests/src/Kernel/MediaFileWidgetTest.php
new file mode 100644
index 0000000000..559b3628b1
--- /dev/null
+++ b/core/modules/media/tests/src/Kernel/MediaFileWidgetTest.php
@@ -0,0 +1,54 @@
+ 'user',
+ 'field_name' => 'field_media',
+ 'type' => 'entity_reference',
+ 'settings' => [
+ 'target_type' => 'media',
+ ],
+ ]);
+ $field_storage->save();
+
+ $field = FieldConfig::create([
+ 'bundle' => 'user',
+ 'field_storage' => $field_storage,
+ 'settings' => [
+ 'handler' => 'default:media',
+ 'handler_settings' => [
+ 'target_bundles' => NULL,
+ ],
+ ],
+ ]);
+ $field->save();
+
+ $this->assertTrue(MediaFileWidget::isApplicable($field));
+
+ // Assert that the widget cannot be used on a string field, or an entity
+ // reference field that does not reference media items.
+ $fields = $this->container
+ ->get('entity_field.manager')
+ ->getBaseFieldDefinitions('user');
+
+ $this->assertFalse(MediaFileWidget::isApplicable($fields['name']));
+ $this->assertFalse(MediaFileWidget::isApplicable($fields['roles']));
+ }
+
+}