diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml
index 0b76811..b326f9b 100644
--- a/core/modules/media/config/schema/media.schema.yml
+++ b/core/modules/media/config/schema/media.schema.yml
@@ -36,6 +36,10 @@ media.type.*:
sequence:
type: string
+media.handler.file:
+ type: media.handler.field_aware
+ label: 'File handler configuration'
+
action.configuration.media_delete_action:
type: action_configuration_default
label: 'Delete media configuration'
diff --git a/core/modules/media/js/media_file_widget.js b/core/modules/media/js/media_file_widget.js
new file mode 100755
index 0000000..1a3576a
--- /dev/null
+++ b/core/modules/media/js/media_file_widget.js
@@ -0,0 +1,14 @@
+(function (Drupal) {
+
+ "use strict";
+
+ Drupal.AjaxCommands.prototype.open_url = function (ajax, response, status) {
+ Drupal.ajax({
+ dialog: { width: '70%', height: 500 },
+ dialogType: 'modal',
+ selector: 'body',
+ url: response.url
+ }).execute();
+ };
+
+})(Drupal);
diff --git a/core/modules/media/media.field.inc b/core/modules/media/media.field.inc
new file mode 100644
index 0000000..f75f657
--- /dev/null
+++ b/core/modules/media/media.field.inc
@@ -0,0 +1,115 @@
+ t('Display'),
+ 'class' => array('checkbox'),
+ );
+ }
+ $headers[] = t('Weight');
+ $headers[] = t('Operations');
+
+ // Get our list of widgets in order (needed when the form comes back after
+ // preview or failed validation).
+ $widgets = array();
+ foreach (Element::children($element['selection']) as $key) {
+ $widgets[] = &$element['selection'][$key];
+ }
+ usort($widgets, '_field_multiple_value_form_sort_helper');
+
+ $rows = array();
+ foreach ($widgets as $key => &$widget) {
+ // Save the uploading row for last.
+ if (empty($widget['file_field'])) {
+ $widget['#title'] = $element['#file_upload_title'];
+ $widget['#description'] = \Drupal::service('renderer')->renderPlain($element['#file_upload_description']);
+ continue;
+ }
+
+ $file_field = &$widget['file_field'];
+
+ // Delay rendering of the buttons, so that they can be rendered later in the
+ // "operations" column.
+ $operations_elements = array();
+ foreach (Element::children($file_field) as $sub_key) {
+ if (isset($file_field[$sub_key]['#type']) && $file_field[$sub_key]['#type'] == 'submit') {
+ hide($file_field[$sub_key]);
+ $operations_elements[] = &$file_field[$sub_key];
+ }
+ }
+
+ // Delay rendering the weight selector, so that
+ // it can be rendered later in its own column.
+ hide($widget['_weight']);
+
+ // Render everything else together in a column, without the normal wrappers.
+ $file_field['#theme_wrappers'] = array();
+ $information = \Drupal::service('renderer')->render($file_field);
+
+ $widget['_weight']['#attributes']['class'] = array($weight_class);
+ $weight = render($widget['_weight']);
+
+ // Arrange the row with all of the rendered columns.
+ $row = array();
+ $row[] = $information;
+ $row[] = $weight;
+
+ // Show the buttons that had previously been marked as hidden in this
+ // preprocess function. We use show() to undo the earlier hide().
+ foreach (Element::children($operations_elements) as $key) {
+ show($operations_elements[$key]);
+ }
+ $row[] = array(
+ 'data' => $operations_elements,
+ );
+ $rows[] = array(
+ 'data' => $row,
+ 'class' => isset($file_field['#attributes']['class']) ? array_merge($file_field['#attributes']['class'], array('draggable')) : array('draggable'),
+ );
+ }
+
+ $variables['table'] = array(
+ '#type' => 'table',
+ '#header' => $headers,
+ '#rows' => $rows,
+ '#attributes' => array(
+ 'id' => $table_id,
+ ),
+ '#tabledrag' => array(
+ array(
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => $weight_class,
+ ),
+ ),
+ '#access' => !empty($rows),
+ );
+
+ $variables['element'] = $element;
+}
diff --git a/core/modules/media/media.libraries.yml b/core/modules/media/media.libraries.yml
old mode 100644
new mode 100755
index ef286b1..c20d2c7
--- a/core/modules/media/media.libraries.yml
+++ b/core/modules/media/media.libraries.yml
@@ -11,3 +11,10 @@ media_type_form:
'js/media_type_form.js': {}
dependencies:
- core/drupal.form
+
+media_file_widget:
+ version: VERSION
+ js:
+ 'js/media_file_widget.js': {}
+ dependencies:
+ - core/drupal.dialog.ajax
diff --git a/core/modules/media/media.module b/core/modules/media/media.module
index f5eb091..359a6d2 100644
--- a/core/modules/media/media.module
+++ b/core/modules/media/media.module
@@ -45,6 +45,10 @@ function media_theme() {
'file' => 'media.theme.inc',
'template' => 'media',
],
+ 'media_file_widget_multiple' => array(
+ 'render element' => 'element',
+ 'file' => 'media.field.inc',
+ ),
];
}
diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml
old mode 100644
new mode 100755
diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml
old mode 100644
new mode 100755
diff --git a/core/modules/media/src/Ajax/OpenUrlCommand.php b/core/modules/media/src/Ajax/OpenUrlCommand.php
new file mode 100644
index 0000000..a2b4a85
--- /dev/null
+++ b/core/modules/media/src/Ajax/OpenUrlCommand.php
@@ -0,0 +1,40 @@
+url = $url instanceof Url ? $url->toString() : $url;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ return [
+ 'command' => 'open_url',
+ 'url' => $this->url,
+ ];
+ }
+
+}
diff --git a/core/modules/media/src/Controller/MediaController.php b/core/modules/media/src/Controller/MediaController.php
new file mode 100644
index 0000000..7ab08de
--- /dev/null
+++ b/core/modules/media/src/Controller/MediaController.php
@@ -0,0 +1,133 @@
+request = $request;
+ $build = parent::addPage($entity_type_id);
+
+ if ($build instanceof Response) {
+ return $build;
+ }
+
+ $bundles = &$build['#bundles'];
+
+ $field_id = $this->request->get('field_id');
+ if ($field_id) {
+ $handler_settings = $this->entityTypeManager
+ ->getStorage('field_config')
+ ->load($field_id)
+ ->getSetting('handler_settings');
+
+ // Only allow bundles that can be referenced by the field.
+ $bundles = array_intersect_key($bundles, array_flip($handler_settings['target_bundles']));
+ }
+
+ $fids = $this->request->get('fids');
+ if ($fids) {
+ /** @var \Drupal\file\FileInterface $file */
+ $file = $this->entityTypeManager
+ ->getStorage('file')
+ ->load($fids[0]);
+
+ // Only allow bundles whose source field supports the file.
+ $bundles = array_intersect_key($bundles, $this->getSupportedTypesForFile($file, array_keys($bundles)));
+ }
+
+ if (count($bundles) === 1) {
+ $route_parameters = [
+ 'media_type' => key($bundles),
+ ];
+ return $this->redirect('entity.media.add_form', $route_parameters, [], 302);
+ }
+ else {
+ $bundles = $this->propagate($bundles);
+ return $build;
+ }
+ }
+
+ protected function propagate(array $bundles) {
+ foreach ($bundles as $id => $element) {
+ $url_options = [
+ 'query' => $this->request->query->all(),
+ 'attributes' => [
+ 'class' => ['use-ajax'],
+ 'data-dialog-type' => 'modal',
+ ],
+ ];
+ $route_parameters = ['media_type' => $id];
+ $url = Url::fromRoute('entity.media.add_form', $route_parameters, $url_options);
+ $bundles[$id]['add_link']->setUrl($url);
+ }
+ return $bundles;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function redirect($route_name, array $route_parameters = [], array $options = [], $status = 302) {
+ $options = array_merge_recursive($options, [
+ 'query' => $this->request->query->all(),
+ ]);
+ $redirect = parent::redirect($route_name, $route_parameters, $options, $status);
+
+ if ($this->isAjaxRequest()) {
+ return (new AjaxResponse)
+ ->addCommand(new OpenUrlCommand($redirect->getTargetUrl()));
+ }
+ else {
+ return $redirect;
+ }
+ }
+
+ protected function isAjaxRequest() {
+ return in_array(
+ $this->request->get(MainContentViewSubscriber::WRAPPER_FORMAT),
+ ['drupal_modal', 'drupal_ajax']
+ );
+ }
+
+ protected function getSupportedTypesForFile(FileInterface $file, array $types = NULL) {
+ /** @var \Drupal\media\MediaTypeInterface[] $types */
+ $types = $this->entityTypeManager
+ ->getStorage('media_type')
+ ->loadMultiple($types);
+
+ $extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
+
+ return array_filter($types, function (MediaTypeInterface $type) use ($extension) {
+ $handler = $type->getHandler();
+ if ($handler instanceof SourceFieldInterface) {
+ $field = $handler->getSourceField($type);
+ if (in_array($field->getType(), ['file', 'image'])) {
+ return in_array($extension, explode(' ', $field->getSetting('file_extensions')));
+ }
+ }
+ return FALSE;
+ });
+ }
+
+}
diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php
index cbe19a8..9984c34 100644
--- a/core/modules/media/src/Entity/Media.php
+++ b/core/modules/media/src/Entity/Media.php
@@ -41,7 +41,7 @@
* "translation" = "Drupal\content_translation\ContentTranslationHandler",
* "views_data" = "Drupal\media\MediaViewsData",
* "route_provider" = {
- * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
+ * "html" = "Drupal\media\Routing\HtmlRouteProvider",
* }
* },
* base_table = "media",
diff --git a/core/modules/media/src/MediaForm.php b/core/modules/media/src/MediaForm.php
old mode 100644
new mode 100755
index 7bd1cea..a1befc1
--- a/core/modules/media/src/MediaForm.php
+++ b/core/modules/media/src/MediaForm.php
@@ -2,8 +2,14 @@
namespace Drupal\media;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\CloseModalDialogCommand;
+use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Entity\ContentEntityForm;
+use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\media\Ajax\OpenUrlCommand;
/**
* Form controller for the media edit forms.
@@ -30,6 +36,19 @@ class MediaForm extends ContentEntityForm {
protected function prepareEntity() {
parent::prepareEntity();
+ $fids = $this->getRequest()->query->get('fids');
+ if ($fids) {
+ // @todo: Fix file upload location.
+ $file = $this->entityTypeManager
+ ->getStorage('file')
+ ->load($fids[0]);
+
+ $handler = $this->entity->getHandler();
+ if ($handler instanceof SourceFieldInterface) {
+ $this->entity->set($handler->getSourceField($this->entity->bundle->entity)->getName(), $file);
+ }
+ }
+
// Set up default values, if required.
if (!$this->getEntity()->isNew()) {
$this->entity->setOwnerId($this->currentUser()->id());
@@ -88,6 +107,12 @@ protected function actions(array $form, FormStateInterface $form_state) {
$element = parent::actions($form, $form_state);
$media = $this->entity;
+ // Add extra submit handler for ajax requests.
+ $element['submit']['#submit'][] = '::afterSave';
+ $element['submit']['#ajax'] = [
+ 'callback' => '::ajaxModalSubmit',
+ ];
+
// Add a "Publish" button.
$element['publish'] = $element['submit'];
// If the "Publish" button is clicked, we want to update the status to
@@ -132,6 +157,9 @@ protected function actions(array $form, FormStateInterface $form_state) {
$element['delete']['#access'] = $media->access('delete');
$element['delete']['#weight'] = 100;
+ unset($element['publish']['#dropbutton']);
+ unset($element['unpublish']['#dropbutton']);
+
return $element;
}
@@ -181,4 +209,56 @@ public function save(array $form, FormStateInterface $form_state) {
return $saved;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function afterSave(array &$form, FormStateInterface $form_state) {
+ $form_state->disableRedirect();
+ $form_state->setValue('entity_id', $this->entity->id());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function ajaxModalSubmit(array &$form, FormStateInterface $form_state) {
+ $response = new AjaxResponse();
+
+ $field_id = $this->getRequest()->query->get('field_id');
+ $fids = array_slice($this->getRequest()->query->get('fids'), 1);
+ $entity_id = $form_state->getValue('entity_id');
+
+ // Check if we have a saved entity.
+ if ($entity_id) {
+ if ($field_id) {
+ $field_name = $this->entityTypeManager
+ ->getStorage('field_config')
+ ->load($field_id)
+ ->getName();
+
+ // Pass id back to field widget and trigger form update.
+ $input_selector = '[data-media-file-input-id="' . $field_name . '"]';
+ $submit_selector = '[data-media-file-submit-id="' . $field_name . '"]';
+ $response->addCommand(new InvokeCommand($input_selector, 'val', [$entity_id]));
+ $response->addCommand(new InvokeCommand($submit_selector, 'trigger', ['mousedown']));
+ }
+ }
+
+ // If we still have file IDs, return to media add list for next file,
+ // or close the modal if all files are added.
+ if ($fids) {
+ $url = Url::fromRoute('entity.media.add_page', [], [
+ 'query' => [
+ 'field_id' => $field_id,
+ 'fids' => $fids,
+ ],
+ ]);
+ $response->addCommand(new OpenUrlCommand($url));
+ }
+ else {
+ $response->addCommand(new CloseModalDialogCommand());
+ }
+
+ 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 100755
index 0000000..7409bfe
--- /dev/null
+++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php
@@ -0,0 +1,736 @@
+elementInfo = $element_info;
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@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'),
+ $container->get('entity_type.manager')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function defaultSettings() {
+ return array(
+ 'progress_indicator' => 'throbber',
+ ) + parent::defaultSettings();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsForm(array $form, FormStateInterface $form_state) {
+ $element['progress_indicator'] = array(
+ '#type' => 'radios',
+ '#title' => t('Progress indicator'),
+ '#options' => array(
+ 'throbber' => t('Throbber'),
+ 'bar' => t('Bar with progress meter'),
+ ),
+ '#default_value' => $this->getSetting('progress_indicator'),
+ '#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'),
+ '#weight' => 16,
+ '#access' => file_progress_implementation(),
+ );
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function settingsSummary() {
+ $summary = array();
+ $summary[] = t('Progress indicator: @progress_indicator', array('@progress_indicator' => $this->getSetting('progress_indicator')));
+ return $summary;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function isApplicable(FieldDefinitionInterface $field_definition) {
+ if (parent::isApplicable($field_definition) && $field_definition->getSetting('target_type') == 'media') {
+ /** @var MediaType[] $bundles */
+ $allowed_types = MediaType::loadMultiple($field_definition->getSetting('handler_settings')['target_bundles']);
+
+ $media_handlers = [];
+ $definitions = \Drupal::service('plugin.manager.field.widget')->getDefinitions();
+ foreach ($definitions as $definition) {
+ if ($definition['class'] == static::class) {
+ $media_handlers = $definition['media_handlers'];
+ }
+ }
+
+ foreach ($allowed_types as $type) {
+ /** @var \Drupal\media\Entity\MediaType $type */
+ if (in_array($type->getHandler()->getPluginId(), $media_handlers)) {
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * Override form method to add AJAX wrapper to widget.
+ *
+ * @param \Drupal\Core\Field\FieldItemListInterface $items
+ * The fields items for the widget.
+ * @param array $form
+ * The widget form elements.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form_state of the widget form.
+ *
+ * @return array
+ * The bsic form structure.
+ */
+ public function form(FieldItemListInterface $items, array &$form, FormStateInterface $form_state, $get_delta = NULL) {
+ $field_name = $this->fieldDefinition->getName();
+ $parents = $form['#parents'];
+ $wrapper_id = $field_name . '-ajax-wrapper';
+
+ // Load the items for form rebuilds from the field state as they might
+ // not be in $form_state['values'] because of validation limitations.
+ $field_state = static::getWidgetState($parents, $field_name, $form_state);
+ if ($field_state && isset($field_state['items'])) {
+ $items->setValue($field_state['items']);
+ }
+ $build = parent::form($items, $form, $form_state);
+
+ // Add a hidden textfield to the form, which is used to pass values
+ // from the modal to the widget. This is similar to how the
+ // EntityReferenceAutocompleteWidget stores values.
+ $build[$field_name . '_media_file_selection'] = [
+ '#type' => 'textfield',
+ '#name' => $field_name . '-media-file-selection',
+ '#attributes' => [
+ 'data-media-file-input-id' => $field_name,
+ 'class' => ['visually-hidden'],
+ ],
+ ];
+ $build[$field_name . '_media_file_update_widget'] = [
+ '#type' => 'submit',
+ '#name' => $field_name . '-media-file-update-button',
+ '#value' => $this->t('Update widget'),
+ '#submit' => [[static::class, 'addItems'], [static::class, 'resetWidget']],
+ '#ajax' => [
+ 'callback' => [static::class, 'updateWidget'],
+ 'wrapper' => $wrapper_id,
+ 'effect' => 'fade',
+ ],
+ '#attributes' => [
+ 'data-media-file-submit-id' => $field_name,
+ 'class' => ['visually-hidden'],
+ ],
+ '#limit_validation_errors' => [array_merge($parents, [$field_name])],
+ ];
+
+ // Wrap the widget in a div so that it can be re-loaded via AJAX.
+ $build['#prefix'] = '
';
+ $build['#suffix'] = '
';
+ return $build;
+ }
+
+ /**
+ * Create a multivalue reference field.
+ *
+ * @param \Drupal\Core\Field\FieldItemListInterface $items
+ * The fields items for the widget.
+ * @param array $form
+ * The widget form elements.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form_state of the widget form.
+ *
+ * @return array
+ * The bsic form structure.
+ */
+ protected function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $form_state) {
+ $field_name = $this->fieldDefinition->getName();
+
+ // Determine the number of widgets to display.
+ $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
+ switch ($cardinality) {
+ case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
+ $max = count($items);
+ $is_multiple = TRUE;
+ break;
+
+ default:
+ $max = $cardinality - 1;
+ $is_multiple = ($cardinality > 1);
+ break;
+ }
+
+ $title = $this->fieldDefinition->getLabel();
+ $description = $this->getFilteredDescription();
+
+ $elements = [
+ 'selection' => [
+ '#type' => 'container',
+ ],
+ ];
+
+ $delta = 0;
+ // Add an element for every existing item.
+ foreach ($items as $item) {
+ $element = [
+ '#title' => $title,
+ '#description' => $description,
+ ];
+ $element = $this->formSingleElement($items, $delta, $element, $form, $form_state);
+
+ if ($element) {
+ // Input field for the delta (drag-n-drop reordering).
+ if ($is_multiple) {
+ // We name the element '_weight' to avoid clashing with elements
+ // defined by widget.
+ $element['_weight'] = array(
+ '#type' => 'weight',
+ '#title' => t('Weight for row @number', array('@number' => $delta + 1)),
+ '#title_display' => 'invisible',
+ // Note: this 'delta' is the FAPI #type 'weight' element's property.
+ '#delta' => $max,
+ '#default_value' => $item->_weight ?: $delta,
+ '#weight' => 100,
+ );
+ }
+
+ $elements['selection'][$delta] = $element;
+ $delta++;
+ }
+ }
+
+ $empty_single_allowed = ($cardinality == 1 && $delta == 0);
+ $empty_multiple_allowed = ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || $delta < $cardinality) && !$form_state->isProgrammed();
+
+ // Add one more empty row for new uploads except when this is a programmed
+ // multiple form as it is not necessary.
+ if ($empty_single_allowed || $empty_multiple_allowed) {
+ $upload_validators = [];
+ $allowed_types = MediaType::loadMultiple($this->fieldDefinition->getSetting('handler_settings')['target_bundles']);
+ foreach ($allowed_types as $type) {
+ /** @var \Drupal\media\Entity\MediaType $type */
+ $source_field = $type->getHandler()->getSourceField($type);
+ $source_data_definition = FieldItemDataDefinition::create($source_field);
+ $file_item = new FileItem($source_data_definition);
+ $upload_validators = array_merge($upload_validators, $file_item->getUploadValidators());
+ }
+
+ // Essentially we use the managed_file type, extended with some
+ // enhancements.
+ $element_info = $this->elementInfo->getInfo('managed_file');
+ $file_upload_help = [
+ '#theme' => 'file_upload_help',
+ '#upload_validators' => $upload_validators,
+ '#cardinality' => $cardinality,
+ ];
+ $elements['upload'][$delta][$field_name . '_media_file_upload'] = [
+ '#type' => 'managed_file',
+ '#required' => $delta == 0 && $this->fieldDefinition->isRequired(),
+ '#upload_validators' => $upload_validators,
+ '#description' => \Drupal::service('renderer')->renderPlain($file_upload_help),
+ '#value_callback' => [get_class($this), 'uploadFieldValue'],
+ '#process' => array_merge($element_info['#process'], [[get_class($this), 'uploadFieldProcess']]),
+ '#progress_indicator' => $this->getSetting('progress_indicator'),
+ '#extended' => TRUE,
+ // Add properties needed by value() and process() methods.
+ '#field_name' => $this->fieldDefinition->getName(),
+ '#bundle' => $this->fieldDefinition->getTargetBundle(),
+ '#entity_type' => $this->fieldDefinition->getTargetEntityTypeId(),
+ '#cardinality' => $cardinality,
+ '#attached' => array_merge_recursive(
+ $element_info['#attached'],
+ ['library' => ['media/media_file_widget']]
+ ),
+ '#multiple' => $cardinality != 1 ? TRUE : FALSE,
+ ];
+ if ($cardinality != 1 && $cardinality != -1) {
+ $elements['upload'][$delta][$field_name . '_media_file_upload']['#element_validate'] = [
+ [static::class, 'validateMultipleCount'],
+ ];
+ }
+ }
+
+ if ($is_multiple) {
+ // The group of elements all-together need some extra functionality after
+ // building up the full list (like draggable table rows).
+ $elements['#file_upload_delta'] = $delta;
+ $elements['#type'] = 'details';
+ $elements['#open'] = TRUE;
+ $elements['#theme'] = 'media_file_widget_multiple';
+ $elements['#theme_wrappers'] = array('details');
+ $elements['#process'] = [[get_class($this), 'processMultiple']];
+ $elements['#title'] = $title;
+
+ $elements['#description'] = $description;
+ $elements['#field_name'] = $field_name;
+ $elements['#language'] = $items->getLangcode();
+
+ // Add some properties that will eventually be added to the file upload
+ // field. These are added here so that they may be referenced easily
+ // through a hook_form_alter().
+ $elements['#file_upload_title'] = t('Add a new file');
+ $elements['#file_upload_description'] = array(
+ '#theme' => 'file_upload_help',
+ '#description' => '',
+ '#upload_validators' => $elements[0]['#upload_validators'],
+ '#cardinality' => $cardinality,
+ );
+ }
+
+ return $elements;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
+ $field_name = $this->fieldDefinition->getName();
+ $wrapper_id = $field_name . '-ajax-wrapper';
+
+ /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items */
+ $referenced_entities = $items->referencedEntities();
+ if (isset($referenced_entities[$delta])) {
+ /** @var \Drupal\media\Entity\Media $media */
+ $media = $referenced_entities[$delta];
+ /** @var \Drupal\field\FieldConfigInterface $source_field */
+ $source_field = $media->getHandler()->getSourceField($media->bundle->entity);
+ /** @var \Drupal\file\Entity\File $file */
+ $file = $media->get($source_field->getName())->entity;
+
+ // We use a hidden field weith the media id and show a managed file
+ // to show the source field value for the media item.
+ $element += [
+ '#field_name' => $field_name,
+ // Add media entity values for storage.
+ 'target_id' => [
+ '#type' => 'hidden',
+ '#value' => $media->id(),
+ ],
+ '#weight' => $delta,
+ // Add file field for display.
+ 'file_field' => [
+ '#type' => 'managed_file',
+ '#value' => [
+ 'fids' => [$file->id()],
+ ],
+ '#process' => [],
+ 'remove_button' => [
+ '#name' => implode('_', [
+ $field_name,
+ $element['#delta'],
+ 'remove_button',
+ ]),
+ '#type' => 'submit',
+ '#value' => t('Remove'),
+ '#remove_delta' => $element['#delta'],
+ '#attributes' => ['class' => ['remove-button']],
+ '#validate' => [],
+ '#submit' => [[static::class, 'removeItem']],
+ '#limit_validation_errors' => [array_merge($form['#parents'], [$field_name])],
+ '#ajax' => [
+ 'callback' => [static::class, 'updateWidget'],
+ 'wrapper' => $wrapper_id,
+ 'effect' => 'fade',
+ ],
+ '#weight' => 1,
+ '#access' => TRUE,
+ ],
+ 'file_' . $delta => [
+ 'filename' => [
+ '#theme' => 'file_link',
+ '#file' => $file,
+ '#weight' => -10,
+ ],
+ ],
+ ],
+ ];
+ }
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) {
+ return isset($element['target_id']) ? $element['target_id'] : FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extractFormValues(FieldItemListInterface $items, array $form, FormStateInterface $form_state) {
+ $field_name = $this->fieldDefinition->getName();
+
+ // Extract the values from $form_state->getValues().
+ $path = array_merge($form['#parents'], array($field_name));
+ $key_exists = NULL;
+ $values = NestedArray::getValue($form_state->getValues(), $path, $key_exists);
+
+ // Remove the file fields used for display.
+ if (isset($values['selection'])) {
+ $values = $values['selection'];
+ }
+
+ if ($key_exists) {
+ // Account for drag-and-drop reordering if needed.
+ if (!$this->handlesMultipleValues()) {
+ // Remove the 'value' of the 'add more' button.
+ unset($values['add_more']);
+
+ // The original delta, before drag-and-drop reordering, is needed to
+ // route errors to the correct form element.
+ foreach ($values as $delta => &$value) {
+ $value['_original_delta'] = $delta;
+ }
+
+ usort($values, function ($a, $b) {
+ return SortArray::sortByKeyInt($a, $b, '_weight');
+ });
+ }
+
+ // Let the widget massage the submitted values.
+ $values = $this->massageFormValues($values, $form, $form_state);
+
+ // Assign the values and remove the empty ones.
+ $items->setValue($values);
+ $items->filterEmptyItems();
+
+ // Put delta mapping in $form_state, so that flagErrors() can use it.
+ $field_state = static::getWidgetState($form['#parents'], $field_name, $form_state);
+ foreach ($items as $delta => $item) {
+ $field_state['original_deltas'][$delta] = isset($item->_original_delta) ? $item->_original_delta : $delta;
+ unset($item->_original_delta, $item->_weight);
+ }
+ static::setWidgetState($form['#parents'], $field_name, $form_state, $field_state);
+ }
+ }
+
+ /**
+ * Form API callback: Processes a group of file_generic field elements.
+ *
+ * Adds the weight field to each row so it can be ordered and adds a new Ajax
+ * wrapper around the entire group so it can be replaced all at once.
+ *
+ * This method on is assigned as a #process callback in formMultipleElements()
+ * method.
+ */
+ public static function processMultiple($element, FormStateInterface $form_state, $form) {
+ $element_children = Element::children($element, TRUE);
+ foreach ($element_children as $delta => $key) {
+ // The title needs to be assigned to the upload field so that validation
+ // errors include the correct widget label.
+ $element[$key]['#title'] = $element['#title'];
+ }
+ return $element;
+ }
+
+ /**
+ * Form element validation callback for upload element on file widget. Checks
+ * if user has uploaded more files than allowed.
+ *
+ * This validator is used only when cardinality not set to 1 or unlimited.
+ */
+ public static function validateMultipleCount($element, FormStateInterface $form_state, $form) {
+ $values = NestedArray::getValue($form_state->getValues(), $element['#parents']);
+
+ $array_parents = $element['#array_parents'];
+ array_pop($array_parents);
+ $previously_uploaded_count = count(Element::children(NestedArray::getValue($form, $array_parents))) - 1;
+
+ $field_storage_definitions = \Drupal::entityManager()->getFieldStorageDefinitions($element['#entity_type']);
+ $field_storage = $field_storage_definitions[$element['#field_name']];
+ $newly_uploaded_count = count($values['fids']);
+ $total_uploaded_count = $newly_uploaded_count + $previously_uploaded_count;
+ if ($total_uploaded_count > $field_storage->getCardinality()) {
+ $keep = $newly_uploaded_count - $total_uploaded_count + $field_storage->getCardinality();
+ $removed_files = array_slice($values['fids'], $keep);
+ $removed_names = array();
+ foreach ($removed_files as $fid) {
+ $file = File::load($fid);
+ $removed_names[] = $file->getFilename();
+ }
+ $args = [
+ '%field' => $field_storage->getName(),
+ '@max' => $field_storage->getCardinality(),
+ '@count' => $total_uploaded_count,
+ '%list' => implode(', ', $removed_names),
+ ];
+ $message = t('Field %field can only hold @max values but there were @count uploaded. The following files have been omitted as a result: %list.', $args);
+ drupal_set_message($message, 'warning');
+ $values['fids'] = array_slice($values['fids'], 0, $keep);
+ NestedArray::setValue($form_state->getValues(), $element['#parents'], $values);
+ }
+ }
+
+ /**
+ * Process callback for the upload field.
+ */
+ public static function uploadFieldProcess($element, FormStateInterface $form_state, $form) {
+ // Override the ajax callback on the button.
+ $element['upload_button']['#ajax']['callback'] = [get_called_class(), 'uploadFieldAjaxCallback'];
+ return $element;
+ }
+
+ /**
+ * Value callback for the upload field.
+ */
+ public static function uploadFieldValue($element, $input, FormStateInterface $form_state) {
+ // We depend on the managed file element to handle uploads.
+ $return = ManagedFile::valueCallback($element, $input, $form_state);
+ // Ensure that all the required properties are returned even if empty.
+ $return += array(
+ 'fids' => array(),
+ );
+ return $return;
+ }
+
+ /**
+ * Ajax callback for the upload field.
+ *
+ * This ajax callback takes care of the following things:
+ * - Add commands from ManagedFile::uploadAjaxCallback() to handle
+ * file uploads.
+ * - Pass the file IDs to a modal.
+ *
+ * @param array $form
+ * The build form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ * @param \Symfony\Component\HttpFoundation\Request $request
+ * The current request.
+ *
+ * @return \Drupal\Core\Ajax\AjaxResponse
+ * The ajax response for the widget.
+ */
+ public static function uploadFieldAjaxCallback(&$form, FormStateInterface &$form_state, Request $request) {
+ $response = new AjaxResponse();
+
+ $form_parents = explode('/', $request->query->get('element_parents'));
+ $element = NestedArray::getValue($form, $form_parents);
+
+ // Open modal to add media.
+ $url = Url::fromRoute('entity.media.add_page', [], [
+ 'query' => [
+ 'field_id' => sprintf(
+ '%s.%s.%s',
+ $element['#entity_type'],
+ $element['#bundle'],
+ $element['#field_name']
+ ),
+ 'fids' => $element['#value']['fids'],
+ ],
+ ]);
+ $response->addCommand(new OpenUrlCommand($url));
+
+ return $response;
+ }
+
+ /**
+ * Submission handler for the hidden "Update widget" button.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public static function addItems($form, FormStateInterface $form_state) {
+ $button = $form_state->getTriggeringElement();
+
+ // Go one level up in the form, to the widgets container.
+ $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1))['widget'];
+ $field_name = $element['#field_name'];
+ $parents = $element['#field_parents'];
+
+ // Get the new media ids passed to our hidden button.
+ $user_input = $form_state->getUserInput();
+ $input_key = $field_name . '-media-file-selection';
+ if (isset($user_input[$input_key])) {
+ $values = $form_state->getValue($field_name);
+ $field_state = static::getWidgetState($parents, $field_name, $form_state);
+
+ // Add new media entity to field.
+ $ids = explode(',', $user_input[$input_key]);
+ /** @var \Drupal\media\MediaInterface[] $media */
+ $media = \Drupal::entityTypeManager()->getStorage('media')->loadMultiple($ids);
+ foreach ($media as $media_item) {
+ if ($media && $media_item->access('view')) {
+ $values['selection'][] = [
+ 'target_id' => $media_item->id(),
+ ];
+ }
+ }
+
+ $field_state['items'] = $values['selection'];
+ $field_state['items_count'] = count($field_state['items']);
+
+ static::setWidgetState($parents, $field_name, $form_state, $field_state);
+
+ $form_state->setRebuild();
+ }
+ }
+
+ /**
+ * Submission handler for the hidden "Update widget" button.
+ *
+ * Resets file upload and selection field for widget.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public static function resetWidget($form, FormStateInterface $form_state) {
+ $button = $form_state->getTriggeringElement();
+
+ // Go one level up in the form, to the widgets container.
+ $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1))['widget'];
+ $field_name = $element['#field_name'];
+
+ $values = $form_state->getValue($field_name);
+ unset($values['upload']);
+ $form_state->setValue($field_name, $values);
+
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Submit callback for remove buttons.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ */
+ public static function removeItem($form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+
+ $parents = $triggering_element['#array_parents'];
+ $parents = array_slice($parents, 0, -4);
+ $element = NestedArray::getValue($form, $parents);
+ $field_name = $element['#field_name'];
+ $parents = $element['#field_parents'];
+ $delta = $triggering_element['#remove_delta'];
+
+ // Find and remove correct entity.
+ $values = $form_state->getValue($field_name);
+ $field_state = static::getWidgetState($parents, $field_name, $form_state);
+ if (isset($values['selection'])) {
+ if (isset($values['selection'][$delta])) {
+ array_splice($values['selection'], $delta, 1);
+ $field_state['items'] = $values['selection'];
+ $field_state['items_count'] = count($field_state['items']);
+ }
+ }
+ $form_state->setValue($field_name, $values);
+ static::setWidgetState($parents, $field_name, $form_state, $field_state);
+
+ // Rebuild form.
+ $form_state->setRebuild();
+ }
+
+ /**
+ * AJAX callback to update the widget when the selection changes.
+ *
+ * @param array $form
+ * The form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The form state.
+ *
+ * @return array
+ * An array representing the updated widget.
+ */
+ public static function updateWidget($form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ $length = isset($triggering_element['#remove_delta']) ? -5 : -1;
+ $parents = $triggering_element['#array_parents'];
+ $parents = array_slice($parents, 0, $length);
+ $element = NestedArray::getValue($form, $parents);
+ return $element;
+ }
+
+}
diff --git a/core/modules/media/src/Routing/HtmlRouteProvider.php b/core/modules/media/src/Routing/HtmlRouteProvider.php
new file mode 100644
index 0000000..1432023
--- /dev/null
+++ b/core/modules/media/src/Routing/HtmlRouteProvider.php
@@ -0,0 +1,22 @@
+setDefault('_controller', MediaController::class . '::addPage');
+ }
+ return $route;
+ }
+
+}
diff --git a/core/modules/media/templates/media-file-widget-multiple.html.twig b/core/modules/media/templates/media-file-widget-multiple.html.twig
new file mode 100644
index 0000000..eb5d9f0
--- /dev/null
+++ b/core/modules/media/templates/media-file-widget-multiple.html.twig
@@ -0,0 +1,16 @@
+{#
+/**
+ * @file
+ * Default theme implementation to display a multi media form widget.
+ *
+ * Available variables:
+ * - table: Table of previously uploaded files.
+ * - element: The form element for uploading another file.
+ *
+ * @see template_preprocess_file_widget_multiple()
+ *
+ * @ingroup themeable
+ */
+#}
+{{ table }}
+{{ element }}