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/js/ajax.js b/core/modules/media/js/ajax.js
new file mode 100644
index 0000000000..f911b89a7e
--- /dev/null
+++ b/core/modules/media/js/ajax.js
@@ -0,0 +1,35 @@
+(function ($, Drupal, drupalSettings) {
+
+ /**
+ * JavaScript handler for opening of modal add media form.
+ *
+ * TODO: es6.js!!!
+ *
+ * @param ajax
+ * @param response
+ * @param status
+ */
+ Drupal.AjaxCommands.prototype.openModalMediaForm = function (ajax, response, status) {
+ var openDialogAjax = new Drupal.Ajax(
+ null,
+ null,
+ {
+ progress: {
+ type: 'throbber'
+ },
+ url: '/media/add-modal',
+ dialogType: 'modal',
+ submit: {
+ file_ids: response.file_ids,
+ field_widget_id: response.field_widget_id,
+ dialogOptions: {
+ width: 800
+ }
+ }
+ }
+ );
+
+ openDialogAjax.execute();
+ };
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/modules/media/media.libraries.yml b/core/modules/media/media.libraries.yml
index 72496a3233..297f7b27ca 100644
--- a/core/modules/media/media.libraries.yml
+++ b/core/modules/media/media.libraries.yml
@@ -11,3 +11,8 @@ type_form:
js/type_form.js: {}
dependencies:
- core/drupal.form
+
+open_modal_media_form:
+ version: VERSION
+ js:
+ js/ajax.js: {}
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index dc2d898e3c..9b7a906a41 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -5,13 +5,18 @@
* Provides media items.
*/
+use Drupal\Component\Utility\Unicode;
use Drupal\Core\Access\AccessResult;
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\field\FieldConfigInterface;
+use Drupal\file\FileInterface;
+use Drupal\file\Plugin\Field\FieldType\FileItem;
+use Drupal\media\Entity\MediaType;
+use Drupal\media\MediaTypeInterface;
/**
* Implements hook_help().
@@ -75,6 +80,90 @@ function media_theme_suggestions_media(array $variables) {
}
/**
+ * TODO: Move into service!
+ *
+ * Get availalbe list of media types for file extension.
+ *
+ * @param string $extension
+ * Extension of file.
+ *
+ * @return array
+ * Returns associative array with (typeId => Label mapping).
+ */
+function media_get_types_by_extension($extension) {
+ static $media_types;
+
+ if (!isset($media_types)) {
+ $media_types = \Drupal::entityTypeManager()
+ ->getStorage('media_type')
+ ->getQuery()
+ ->execute();
+
+ $media_types = array_filter($media_types, [
+ \Drupal::entityTypeManager()->getAccessControlHandler('media'),
+ 'createAccess',
+ ]);
+
+ $media_types = MediaType::loadMultiple($media_types);
+
+ $media_types = array_filter($media_types, function (MediaTypeInterface $media_type) {
+ $item_class = $media_type
+ ->getSource()
+ ->getSourceFieldDefinition($media_type)
+ ->getItemDefinition()
+ ->getClass();
+
+ return is_a($item_class, FileItem::class, TRUE);
+ });
+ }
+
+ $extension = Unicode::strtolower($extension);
+
+ $allowed_media_types = [];
+
+ /** @var \Drupal\media\MediaTypeInterface $media_type */
+ foreach ($media_types as $media_type) {
+ $extensions = $media_type
+ ->getSource()
+ ->getSourceFieldDefinition($media_type)
+ ->getSetting('file_extensions');
+
+ $extensions = Unicode::strtolower($extensions);
+ $extensions = preg_split('/\s+/', $extensions);
+
+ if (in_array($extension, $extensions)) {
+ $allowed_media_types[$media_type->id()] = $media_type->label();
+ }
+ }
+
+ return $allowed_media_types;
+}
+
+/**
+ * TODO: Move into some validation class, or whatever!
+ *
+ * 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) {
+ $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
+
+ $availableTypes = media_get_types_by_extension($extension);
+ if (empty($availableTypes)) {
+ return [
+ t('%filename could not be matched to any media types.', [
+ '%filename' => $file->getFilename(),
+ ]),
+ ];
+ }
+}
+
+/**
* Prepares variables for media templates.
*
* Default template: media.html.twig.
diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml
index 9fbadeff29..774d4aedae 100644
--- a/core/modules/media/media.routing.yml
+++ b/core/modules/media/media.routing.yml
@@ -19,3 +19,10 @@ entity.media.revision:
requirements:
_access_media_revision: 'view'
media: \d+
+
+entity.media.add_modal:
+ path: '/media/add-modal'
+ defaults:
+ _form: '\Drupal\media\Form\MediaModalForm'
+ requirements:
+ _permission: 'administer media'
\ No newline at end of file
diff --git a/core/modules/media/src/Ajax/OpenModalMediaForm.php b/core/modules/media/src/Ajax/OpenModalMediaForm.php
new file mode 100644
index 0000000000..9635a08d67
--- /dev/null
+++ b/core/modules/media/src/Ajax/OpenModalMediaForm.php
@@ -0,0 +1,50 @@
+fileIds = $fileIds;
+ $this->widgetId = $widgetId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ return [
+ 'command' => 'openModalMediaForm',
+ 'file_ids' => $this->fileIds,
+ 'field_widget_id' => $this->widgetId,
+ ];
+ }
+
+}
diff --git a/core/modules/media/src/Form/MediaModalForm.php b/core/modules/media/src/Form/MediaModalForm.php
new file mode 100644
index 0000000000..410a990d53
--- /dev/null
+++ b/core/modules/media/src/Form/MediaModalForm.php
@@ -0,0 +1,292 @@
+set('queued_fids', $form_state->getUserInput()['file_ids']);
+ $form_state->set('field_widget_id', $form_state->getUserInput()['field_widget_id']);
+
+ $form_state->set('processed_media_ids', []);
+
+ $form_state->setCached(TRUE);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['#prefix'] = '
';
+ $form['#suffix'] = '
';
+
+ // Initialize form state if there are no queued files.
+ if (empty($form_state->get('queued_fids'))) {
+ $this->initFormState($form_state);
+ }
+ $file_ids = $form_state->get('queued_fids');
+ $file = $this->getFileById($file_ids[0]);
+
+ // Create drop-down list with list of available media types.
+ $list_of_media_types = $this->getMediaTypeList($file_ids[0]);
+ $form['media_type'] = [
+ '#id' => 'media-modal-form-media-type',
+ '#prefix' => '',
+ '#suffix' => '
',
+ '#type' => 'select',
+ '#title' => $this->t('Media type'),
+ '#options' => $list_of_media_types,
+ '#ajax' => [
+ 'callback' => '::ajaxChangeType',
+ 'wrapper' => 'media-modal-form',
+ ],
+ ];
+
+ // Get selected media type.
+ $trigger_element = $form_state->getTriggeringElement();
+ if (!empty($trigger_element) && $trigger_element['#name'] === 'media_type') {
+ $selected_media_type = $trigger_element['#value'];
+ }
+ else {
+ reset($list_of_media_types);
+ $selected_media_type = key($list_of_media_types);
+ }
+
+ // Prepare inner media form.
+ $form['#process'] = \Drupal::service('element_info')->getInfoProperty('form', '#process', []);
+ $form['#process'][] = '::processForm';
+
+ // Create media entity.
+ $media = $this->createMediaFromFile($selected_media_type, $file);
+ $form[static::INNER_FORM_ID] = $this->getInnerMediaForm($form, $form_state, $media);
+
+ // Create action buttons for modal dialog.
+ $form['actions'] = [
+ '#type' => 'actions',
+ ];
+
+ $form['actions']['submit_form'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save'),
+ '#validate' => ['::validateForm'],
+ '#submit' => ['::submitForm'],
+ '#ajax' => [
+ 'callback' => '::ajaxSubmitForm',
+ 'wrapper' => 'media-modal-form',
+ 'event' => 'click',
+ ],
+ ];
+
+ return $form;
+ }
+
+ protected static function createInnerFormState(FormStateInterface $form_state, FormInterface $form_object, $key) {
+ $inner_form_state = new FormState();
+ $inner_form_state->setFormObject($form_object);
+ $form_state->set([static::INNER_FORM_STATE_KEY, $key], $inner_form_state);
+
+ return $inner_form_state;
+ }
+
+ protected static function getInnerFormState(FormStateInterface $form_state, $key) {
+ /** @var \Drupal\Core\Form\FormStateInterface $inner_form_state */
+ $inner_form_state = $form_state->get([static::INNER_FORM_STATE_KEY, $key]);
+
+ $inner_form_state->setCompleteForm($form_state->getCompleteForm());
+ $inner_form_state->setValues($form_state->getValues() ?: []);
+ $inner_form_state->setUserInput($form_state->getUserInput() ?: []);
+
+ return $inner_form_state;
+ }
+
+ public function processForm(array &$element, FormStateInterface &$form_state, array &$complete_form) {
+ $inner_form_state = static::getInnerFormState($form_state, static::INNER_FORM_ID);
+
+ foreach ($inner_form_state->get('#process') as $callback) {
+ $element[static::INNER_FORM_ID] = call_user_func_array($inner_form_state->prepareCallback($callback), array(
+ &$element[static::INNER_FORM_ID],
+ &$inner_form_state,
+ &$complete_form,
+ ));
+ }
+
+ return $element;
+ }
+
+ protected function getInnerMediaForm(array $form, FormStateInterface $form_state, $media) {
+ $inner_form_object = \Drupal::entityTypeManager()
+ ->getFormObject('media', 'edit')
+ ->setEntity($media);
+
+ $form_state->set('inner_form_object', $inner_form_object);
+
+ $inner_form_state = static::createInnerFormState($form_state, $inner_form_object, static::INNER_FORM_ID);
+
+ $inner_form = ['#parents' => [static::INNER_FORM_ID]];
+ $inner_form = $inner_form_object->buildForm($inner_form, $inner_form_state);
+
+ $inner_form['#type'] = 'container';
+ $inner_form['#theme_wrappers'] = \Drupal::service('element_info')
+ ->getInfoProperty('container', '#theme_wrappers', []);
+ unset($inner_form['form_token']);
+
+ if (!empty($inner_form['#process'])) {
+ $inner_form_state->set('#process', $inner_form['#process']);
+ unset($inner_form['#process']);
+ }
+ else {
+ $inner_form_state->set('#process', []);
+ }
+
+ // Remove actions from subform, because wrapping form will handle it.
+ if (!empty($inner_form['actions'])) {
+ if (isset($inner_form['actions'][static::MAIN_SUBMIT_BUTTON])) {
+ $inner_form['#submit'] = $inner_form['actions'][static::MAIN_SUBMIT_BUTTON]['#submit'];
+ }
+ unset($inner_form['actions']);
+ }
+
+ $inner_form['#id'] = 'media-modal-form-inner-form';
+
+ return $inner_form;
+ }
+
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\Core\Form\FormValidatorInterface $form_validator */
+ $form_validator = \Drupal::service('form_validator');
+
+ if ($form_state->getTriggeringElement()['#name'] === 'op') {
+ $inner_form_object = $form_state->get('inner_form_object');
+ $inner_form_state = static::getInnerFormState($form_state, static::INNER_FORM_ID);
+
+ // Pass through both the form elements validation and the form object
+ // validation.
+ $inner_form_object->validateForm($form[static::INNER_FORM_ID], $inner_form_state);
+ $form_validator->validateForm($inner_form_object->getFormId(), $form[static::INNER_FORM_ID], $inner_form_state);
+
+ foreach ($inner_form_state->getErrors() as $error_element_path => $error) {
+ $form_state->setErrorByName(static::INNER_FORM_ID . '][' . $error_element_path, $error);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\Core\Form\FormSubmitterInterface $form_submitter */
+ $form_submitter = \Drupal::service('form_submitter');
+
+ $inner_form_state = static::getInnerFormState($form_state, static::INNER_FORM_ID);
+ $inner_form_state->setSubmitted();
+ $form_submitter->doSubmitForm($form[static::INNER_FORM_ID], $inner_form_state);
+
+ $file_ids = $form_state->get('queued_fids');
+ array_shift($file_ids);
+ $form_state->set('queued_fids', $file_ids);
+
+ $processed_ids = $form_state->get('processed_media_ids');
+ $processed_ids[] = $form_state->get('inner_form_object')->getEntity()->id();
+ $form_state->set('processed_media_ids', $processed_ids);
+
+ if (!empty($file_ids)) {
+ $form_state->setRebuild();
+ }
+ }
+
+ protected function createMediaFromFile($type, $file) {
+ $media_item = Media::create([
+ 'bundle' => $type,
+ ]);
+
+ $configuration = $media_item->getSource()->getConfiguration();
+ $media_item->set($configuration['source_field'], $file);
+
+ return $media_item;
+ }
+
+ protected function getFileById($fileId) {
+ /** @var \Drupal\file\FileStorage $file_storage */
+ $file_storage = \Drupal::service('entity_type.manager')->getStorage('file');
+
+ return $file_storage->load($fileId);
+ }
+
+ protected function getMediaTypeList($file_id) {
+ $file = $this->getFileById($file_id);
+
+ $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
+
+ return media_get_types_by_extension($extension);
+ }
+
+ public function ajaxSubmitForm(array &$form, FormStateInterface $form_state) {
+ $fileIds = $form_state->get('queued_fids');
+
+ // All files are processed, close dialog and send information to caller.
+ if (empty($fileIds)) {
+ $response = new AjaxResponse();
+
+ // TODO: Add exeuction of JS Command that will send processed media IDs to
+ // widget.
+ $response->addCommand(new CloseModalDialogCommand());
+ }
+ else {
+ $response = $this->ajaxChangeType($form, $form_state);
+ }
+
+ return $response;
+ }
+
+ public function ajaxChangeType(array &$form, FormStateInterface $form_state) {
+ $response = new AjaxResponse();
+
+ $selection_button_html = trim(drupal_render_root($form['media_type']));
+ $response->addCommand(new ReplaceCommand('#media-modal-form-media-type-wrapper', $selection_button_html));
+
+ $media_form_html = trim(drupal_render_root($form[static::INNER_FORM_ID]));
+ $response->addCommand(new ReplaceCommand('#media-modal-form-inner-form', $media_form_html));
+
+ return $response;
+ }
+
+}
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..24e4bd473d
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
@@ -0,0 +1,118 @@
+ TRUE,
+ ];
+ return $settings + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function isApplicable(FieldDefinitionInterface $field_definition) {
+ if ($field_definition->getType() == 'entity_reference') {
+ return $field_definition->getFieldStorageDefinition()->getSetting('target_type') == 'media';
+ }
+ }
+
+ /**
+ * {@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();
+
+ $element['file'] = [
+ '#type' => 'managed_file',
+ '#multiple' => $cardinality !== $field_storage_definition::CARDINALITY_UNLIMITED,
+ // Hide the file element if the field is at capacity.
+ '#access' => $items->isEmpty() || ($cardinality > 0 && count($items) < $cardinality),
+ '#upload_validators' => [
+ 'media_validate_file_upload' => [],
+ ],
+ ];
+
+ $element_info = \Drupal::service('element_info')->getInfo('managed_file');
+ $element['file']['#process'] = array_merge($element_info['#process'], [
+ [static::class, 'processFileElement'],
+ ]);
+
+ return $element;
+ }
+
+ public static function processFileElement(array $element) {
+ $element['upload_button']['#ajax']['callback'][0] = static::class;
+ $element['upload_button']['#submit'][] = [static::class, 'onFileUpload'];
+ return $element;
+ }
+
+ public static function onFileUpload(array &$form, FormStateInterface $form_state) {
+ $upload_button = $form_state->getTriggeringElement();
+ $key = $upload_button['#array_parents'];
+ array_splice($key, -1, 1, ['#value', 'fids']);
+ $fids = NestedArray::getValue($form, $key);
+
+ $form_state->set('file_ids', $fids);
+ }
+
+ public static function uploadAjaxCallback(array &$form, FormStateInterface $form_state, Request $request) {
+ $response = new AjaxResponse();
+
+ $fileIds = $form_state->get('file_ids') ?: [];
+
+ // TODO: Get correct ID for widget and send it, it's needed for modal to
+ // return data back to widget.
+ $response->addCommand(new OpenModalMediaForm($fileIds, 'test-widget-id'));
+ $response->setAttachments([
+ 'library' => ['media/open_modal_media_form'],
+ ]);
+
+ return $response;
+ }
+
+}
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..4b7686f247
--- /dev/null
+++ b/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php
@@ -0,0 +1,105 @@
+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();
+ }
+
+ public function testWidget() {
+ $assert = $this->assertSession();
+
+ $account = $this->drupalCreateUser([
+ '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'));
+
+ $this->getSession()->getPage()->attachFileToField($file_element->getAttribute('name'), __DIR__ . '/../../fixtures/example_1.jpeg');
+ $this->waitUntilVisible('#drupal-modal', 10000);
+ $assert->elementExists('css', '#drupal-modal form');
+
+ // 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']));
+ }
+
+}