1c1 < diff --git a/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php b/core/lib/Drupal/Core/Ajax/OpenDialogUrlCommand.php --- > diff --git a/core/modules/media/config/optional/core.entity_form_mode.media.add_inline.yml b/core/modules/media/config/optional/core.entity_form_mode.media.add_inline.yml 3c3 < index 0000000000..7e36a33dcd --- > index 0000000000..e082019a82 5,198c5 < +++ 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 --- > +++ b/core/modules/media/config/optional/core.entity_form_mode.media.add_inline.yml 205c12 < +id: media.add_file --- > +id: media.add_inline 209,235c16,29 < 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; --- > diff --git a/core/modules/media/config/install/core.entity_view_mode.media.full.yml b/core/modules/media/config/optional/core.entity_view_mode.media.full.yml > similarity index 100% > rename from core/modules/media/config/install/core.entity_view_mode.media.full.yml > rename to core/modules/media/config/optional/core.entity_view_mode.media.full.yml > diff --git a/core/modules/media/src/Element/MediaManagedFile.php b/core/modules/media/src/Element/MediaManagedFile.php > new file mode 100644 > index 0000000000..e3fbbe8cab > --- /dev/null > +++ b/core/modules/media/src/Element/MediaManagedFile.php > @@ -0,0 +1,43 @@ > + + > +namespace Drupal\media\Element; > + 237,251c31,32 < 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]; < } < } --- > +use Drupal\file\Element\ManagedFile; > +use Drupal\media\Entity\Media; 254,260c35 < + * Validate uploaded file for media field. < + * < + * @param \Drupal\file\FileInterface $file < + * File that should be validated. < + * < + * @return array < + * Result of validation. --- > + * @FormElement("media_managed_file") 262,263c37,51 < +function media_validate_file_upload(FileInterface $file) { < + $media_types = \Drupal::service('media.type_guesser.file')->getTypesForFile($file); --- > +class MediaManagedFile extends ManagedFile { > + > + public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) { > + if (!empty($element['#value']['target_id'])) { > + // We're editing an entity with a previously-saved Media entity > + // reference(s). In order not to confuse ManagedFile, load those entities' > + // source fids and store them where it expects them. > + $media_entity = Media::load($element['#value']['target_id']); > + $media_source_field = $media_entity->getSource()->getConfiguration()['source_field']; > + $file_value = $media_entity->get($media_source_field)->getValue(); > + foreach ($file_value as $value) { > + $element['#value']['fids'][] = $value['target_id']; > + } > + } > + $element = parent::processManagedFile($element, $form_state, $complete_form); 265,269c53,62 < + if (empty($media_types)) { < + return [ < + t('%filename could not be matched to any media types.', [ < + '%filename' => $file->getFilename(), < + ]), --- > + // We don't want to show the file-upload field for values that already have > + // a file. > + if (!empty($element['#files'])) { > + $element['upload']['#access'] = FALSE; > + } > + > + $mids = isset($element['#value']['mids']) ? $element['#value']['mids'] : []; > + $element['mids'] = [ > + '#type' => 'hidden', > + '#value' => $mids, 271,273d63 < + } < + return []; < +} 275,280c65,66 < +/** < + * 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); < +} --- > + return $element; > + } 282,286d67 < +/** < + * 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); 288,321d68 < 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'] 323c70 < index 49f7283f7e..dea100180c 100644 --- > index 49f7283f7e..2c6862f8a7 100644 326,327c73 < @@ -38,6 +38,7 @@ < * "add" = "Drupal\media\MediaForm", --- > @@ -39,6 +39,7 @@ 330d75 < + * "add_file" = "Drupal\media\Form\AddFileForm", 331a77 > + * "add_inline" = "Drupal\media\Form\MediaInlineForm", 334c80,81 < diff --git a/core/modules/media/src/FileGuesser.php b/core/modules/media/src/FileGuesser.php --- > * "route_provider" = { > diff --git a/core/modules/media/src/Form/MediaInlineForm.php b/core/modules/media/src/Form/MediaInlineForm.php 336c83 < index 0000000000..1f69ee7f24 --- > index 0000000000..988de582a6 338,339c85,86 < +++ b/core/modules/media/src/FileGuesser.php < @@ -0,0 +1,152 @@ --- > +++ b/core/modules/media/src/Form/MediaInlineForm.php > @@ -0,0 +1,154 @@ 342c89 < +namespace Drupal\media; --- > +namespace Drupal\media\Form; 344,346c91,94 < +use Drupal\Component\Utility\Unicode; < +use Drupal\Core\Cache\CacheBackendInterface; < +use Drupal\Core\Cache\UseCacheBackendTrait; --- > +use Drupal\Core\Entity\ContentEntityInterface; > +use Drupal\Core\Entity\Entity\EntityFormDisplay; > +use Drupal\Core\Entity\EntityFieldManagerInterface; > +use Drupal\Core\Entity\EntityTypeInterface; 348,500c96,99 < +use Drupal\file\FileInterface; < +use Drupal\file\Plugin\Field\FieldType\FileItem; < + < +/** < + * Discovers media types which support file uploads. < + */ < +class FileGuesser implements FileGuesserInterface { < + < + use UseCacheBackendTrait; < + < + /** < + * Entity type manager service. < + * < + * @var \Drupal\Core\Entity\EntityTypeManagerInterface < + */ < + protected $entityTypeManager; < + < + /** < + * All media types that can be created by the current user. < + * < + * @var \Drupal\media\MediaTypeInterface[] < + */ < + protected $allowedTypes; < + < + /** < + * Constructs a new FileGuesser. < + * < + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager < + * Entity type manager. < + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend < + * (optional) A cache backend. < + */ < + public function __construct(EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache_backend = NULL) { < + $this->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 @@ < + +use Drupal\Core\Extension\ModuleHandlerInterface; > +use Drupal\Core\Form\FormStateInterface; > +use Drupal\media\MediaInlineFormInterface; > +use Symfony\Component\DependencyInjection\ContainerInterface; 503c102 < + * Discovers media types which support file uploads. --- > + * Media entity inline form handler. 505c104 < +interface FileGuesserInterface { --- > +class MediaInlineForm implements MediaInlineFormInterface { 508c107 < + * Returns the first media type ID that matches a file. --- > + * The entity field manager. 510,517c109 < + * @param \Drupal\file\FileInterface|int $file < + * A file entity or ID. < + * @param string[] $limit < + * (optional) The media type IDs to choose from. If omitted, all media types < + * will be considered. < + * < + * @return string|bool < + * A media type ID, or FALSE if no media types matched. --- > + * @var \Drupal\Core\Entity\EntityFieldManagerInterface 519c111 < + public function guessForFile($file, array $limit = []); --- > + protected $entityFieldManager; 522,528c114 < + * Returns the first media type ID that matches a file extension. < + * < + * @param string $extension < + * A file extension, without the leading period. < + * @param string[] $limit < + * (optional) The media type IDs to choose from. If omitted, all media types < + * will be considered. --- > + * The entity type manager. 530,531c116 < + * @return string|bool < + * A media type ID, or FALSE if no media types matched. --- > + * @var \Drupal\Core\Entity\EntityTypeManagerInterface 533c118 < + public function guessForExtension($extension, array $limit = []); --- > + protected $entityTypeManager; 536,542c121 < + * Returns all media types that match a file. < + * < + * @param \Drupal\file\FileInterface|int $file < + * A file entity or ID. < + * @param string[] $limit < + * (optional) The media type IDs to choose from. If omitted, all media types < + * will be considered. --- > + * The entity type managed by this handler. 544,548c123 < + * @throws \InvalidArgumentException < + * If $file is not a file entity, or the ID of one. < + * < + * @return \Drupal\media\MediaTypeInterface[] < + * All matching media types. --- > + * @var \Drupal\Core\Entity\EntityTypeInterface 550c125 < + public function getTypesForFile($file, array $limit = []); --- > + protected $entityType; 553,559c128 < + * Returns all media types that match a file extension. < + * < + * @param string $extension < + * A file extension, without the leading period. < + * @param string[] $limit < + * (optional) The media type IDs to choose from. If omitted, all media types < + * will be considered. --- > + * Module handler service. 561,562c130 < + * @return \Drupal\media\MediaTypeInterface[] < + * All matching media types. --- > + * @var \Drupal\Core\Extension\ModuleHandlerInterface 564,596c132 < + public function getTypesByExtension($extension, array $limit = []); < + < +} < diff --git a/core/modules/media/src/Form/AddFileForm.php b/core/modules/media/src/Form/AddFileForm.php < new file mode 100644 < index 0000000000..465866fa28 < --- /dev/null < +++ b/core/modules/media/src/Form/AddFileForm.php < @@ -0,0 +1,317 @@ < + + protected $moduleHandler; 599c135 < + * The file guesser service. --- > + * Constructs the inline entity form controller. 601,615c137,144 < + * @var \Drupal\media\FileGuesserInterface < + */ < + protected $fileGuesser; < + < + /** < + * Constructs a ContentEntityForm object. < + * < + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager < + * The entity manager. < + * @param \Drupal\media\FileGuesserInterface $file_guesser < + * The file guesser service. < + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info < + * (optional) The entity type bundle service. < + * @param \Drupal\Component\Datetime\TimeInterface $time < + * (optional) The time service. --- > + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager > + * The entity field manager. > + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager > + * The entity type manager. > + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler > + * The module handler. > + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type > + * The entity type. 617,619c146,150 < + public function __construct(EntityManagerInterface $entity_manager, FileGuesserInterface $file_guesser, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) { < + parent::__construct($entity_manager, $entity_type_bundle_info, $time); < + $this->fileGuesser = $file_guesser; --- > + public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, EntityTypeInterface $entity_type) { > + $this->entityFieldManager = $entity_field_manager; > + $this->entityTypeManager = $entity_type_manager; > + $this->moduleHandler = $module_handler; > + $this->entityType = $entity_type; 625c156 < + public static function create(ContainerInterface $container) { --- > + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { 627,630c158,161 < + $container->get('entity.manager'), < + $container->get('media.type_guesser.file'), < + $container->get('entity_type.bundle.info'), < + $container->get('datetime.time') --- > + $container->get('entity_field.manager'), > + $container->get('entity_type.manager'), > + $container->get('module_handler'), > + $entity_type 635,649d165 < + * 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']; < + } < + < + /** 652,662c168,169 < + 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); --- > + public function getEntityType() { > + return $this->entityType; 668,701c175,196 < + 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); --- > + public function entityForm(array $entity_form, FormStateInterface $form_state) { > + // Build the media entity form. > + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ > + $entity = $entity_form['#entity']; > + $form_display = $this->getFormDisplay($entity, $entity_form['#form_mode']); > + $form_display->buildForm($entity, $entity_form, $form_state); > + $entity_form['#weight'] = 100; > + > + // In the service of keeping this form as user-friendly as possible in the > + // context of a parent entity form, only show required fields. > + /** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */ > + $entity_field_manager = \Drupal::service('entity_field.manager'); > + $field_definitions = $entity_field_manager->getFieldDefinitions('media', 'file'); > + /** @var \Drupal\Core\Field\BaseFieldDefinition $field_definition */ > + foreach ($field_definitions as $field_definition) { > + $field_name = $field_definition->getName(); > + if (!$field_definition->isRequired()) { > + $entity_form[$field_name]['#access'] = FALSE; > + } > + else { > + $entity_form[$field_name]['#element_validate'] = []; > + } 703,709d197 < + // 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']); 712,724c200,201 < + 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(); --- > + // We've already set the just-uploaded file as the value of the source > + // field, so hide that too. 728,734c205 < + /** @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, < + ]; --- > + $entity_form[$source_field]['#access'] = FALSE; 736,772c207,216 < + $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(); --- > + // Inline entities inherit the parent language. > + $langcode_key = $this->entityType->getKey('langcode'); > + if ($langcode_key && isset($entity_form[$langcode_key])) { > + $entity_form[$langcode_key]['#access'] = FALSE; > + } > + if (!empty($entity_form['#translating'])) { > + // Hide the non-translatable fields. > + foreach ($entity->getFieldDefinitions() as $field_name => $definition) { > + if (isset($entity_form[$field_name]) && $field_name != $langcode_key) { > + $entity_form[$field_name]['#access'] = $definition->isTranslatable(); 777,778d220 < + // Hide revision log message field. < + $form['fields']['revision_log_message']['#access'] = FALSE; 780c222 < + return $form; --- > + return $entity_form; 784,797c226 < + * {@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. --- > + * Gets the form display for the given entity. 799,802c228,231 < + * @param array $form < + * The form. < + * @param \Drupal\Core\Form\FormStateInterface $form_state < + * The form state. --- > + * @param \Drupal\Core\Entity\ContentEntityInterface $entity > + * The entity. > + * @param string $form_mode > + * The form mode. 804,805c233,234 < + * @return \Drupal\Core\Ajax\AjaxResponse < + * The AJAX response. --- > + * @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface > + * The form display. 807,808c236,238 < + public function onSave(array &$form, FormStateInterface $form_state) { < + $response = new AjaxResponse(); --- > + protected function getFormDisplay(ContentEntityInterface $entity, $form_mode) { > + return EntityFormDisplay::collectRenderDisplay($entity, $form_mode); > + } 810,814c240,247 < + if ($form_state->getErrors()) { < + return $response->addCommand( < + new BeforeCommand('#entity-fields', ['#type' => 'status_messages']) < + ); < + } --- > +} > diff --git a/core/modules/media/src/MediaInlineFormInterface.php b/core/modules/media/src/MediaInlineFormInterface.php > new file mode 100644 > index 0000000000..fc6ea3ac88 > --- /dev/null > +++ b/core/modules/media/src/MediaInlineFormInterface.php > @@ -0,0 +1,36 @@ > +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; < + } --- > +namespace Drupal\media; 849,858c251,252 < + /** < + * 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; --- > +use Drupal\Core\Entity\EntityHandlerInterface; > +use Drupal\Core\Form\FormStateInterface; 860,863c254,257 < + if ($form_state->getFormObject() instanceof $self) { < + $element['#process'][] = [$self, 'hideRemoveButton']; < + } < + } --- > +/** > + * Defines the interface for inline form handlers. > + */ > +interface MediaInlineFormInterface extends EntityHandlerInterface { 866c260 < + * Hides the remove button. --- > + * Gets the entity type managed by this handler. 868,872c262,263 < + * @param array $element < + * An associative array containing the 'remove_button' render element. < + * < + * @return array < + * An associated array containing the updated render array. --- > + * @return \Drupal\Core\Entity\EntityTypeInterface > + * The entity type. 874,877c265 < + public static function hideRemoveButton(array $element) { < + $element['remove_button']['#access'] = FALSE; < + return $element; < + } --- > + public function getEntityType(); 880c268 < + * Checks whether the revision form fields should be added to the form. --- > + * Builds the entity form. 882,883c270,278 < + * @return bool < + * TRUE if the form field should be added, FALSE otherwise. --- > + * @param array $entity_form > + * The entity form, containing the following basic properties: > + * - #entity: The entity for the current entity form. > + * - #form_mode: The form mode used to display the entity form. > + * - #parents: Identifies the position of the entity form in the overall > + * parent form, and identifies the location where the field values are > + * placed within $form_state->getValues(). > + * @param \Drupal\Core\Form\FormStateInterface $form_state > + * The form state of the parent form. 885,887c280 < + protected function showRevisionUi() { < + return FALSE; < + } --- > + public function entityForm(array $entity_form, FormStateInterface $form_state); 892c285 < index 0000000000..33a779d09a --- > index 0000000000..7ac3eb3c10 895c288 < @@ -0,0 +1,269 @@ --- > @@ -0,0 +1,310 @@ 900,904d292 < +use Drupal\Component\Utility\NestedArray; < +use Drupal\Core\Ajax\OpenDialogUrlCommand; < +use Drupal\Core\Field\TypedData\FieldItemDataDefinition; < +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; < +use Drupal\Core\Field\FieldDefinitionInterface; 906d293 < +use Drupal\Core\Field\WidgetBase; 908,910c295,298 < +use Drupal\Core\Render\RendererInterface; < +use Drupal\Core\Url; < +use Drupal\file\Element\ManagedFile; --- > +use Drupal\file\Entity\File; > +use Drupal\file\Plugin\Field\FieldWidget; > +use Drupal\file\Plugin\Field\FieldWidget\FileWidget; > +use Drupal\Core\Field\TypedData\FieldItemDataDefinition; 911a300 > +use Drupal\media\Entity\Media; 913,914c302,303 < +use Symfony\Component\DependencyInjection\ContainerInterface; < +use Symfony\Component\HttpFoundation\Request; --- > +use Drupal\Component\Utility\NestedArray; > +use Drupal\Core\Link; 923,925c312,313 < + * "entity_reference", < + * }, < + * multiple_values = TRUE, --- > + * "entity_reference" > + * } 928c316 < +class MediaFileWidget extends WidgetBase implements ContainerFactoryPluginInterface { --- > +class MediaFileWidget extends FileWidget { 931,933c319 < + * An associative array describing the 'managed_file' element type. < + * < + * As defined by the plugin.manager.element_info service. --- > + * {@inheritdoc} 935c321,323 < + * @var array --- > + * Uses nearly the same code that FileWidget does here, except that we have to > + * dig down through the target Media entity to *its* file field's upload > + * location and upload validators. 937c325,326 < + protected $managedFileInfo; --- > + public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) { > + $field_settings = $this->getFieldSettings(); 939,944c328,373 < + /** < + * The renderer service. < + * < + * @var \Drupal\Core\Render\RendererInterface < + */ < + protected $renderer; --- > + // The field settings include defaults for the field type. However, this > + // widget is a base class for other widgets (e.g., ImageWidget) that may act > + // on field types without these expected settings. > + $field_settings += [ > + 'display_default' => NULL, > + 'display_field' => NULL, > + 'description_field' => NULL, > + ]; > + > + $defaults = [ > + 'mids' => [], > + 'fids' => [], > + 'display' => (bool) $field_settings['display_default'], > + 'description' => '', > + ]; > + > + /** @var \Drupal\media\Entity\MediaType $media_type */ > + $target_bundle = array_shift($this->fieldDefinition->getSetting('handler_settings')['target_bundles']); > + $media_type = MediaType::load($target_bundle); > + $source = $media_type->getSource(); > + $source_data_definition = FieldItemDataDefinition::create($source->getSourceFieldDefinition($media_type)); > + $file_item = new FileItem($source_data_definition); > + $element_info = $this->elementInfo->getInfo('media_managed_file'); > + > + $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); > + > + $element += [ > + '#type' => 'media_managed_file', > + '#upload_location' => $file_item->getUploadLocation(), > + '#upload_validators' => $file_item->getUploadValidators(), > + '#value_callback' => [get_class($this), 'value'], > + '#process' => array_merge($element_info['#process'], [[get_class($this), 'process']]), > + '#progress_indicator' => $this->getSetting('progress_indicator'), > + // Allows this field to return an array instead of a single value. > + '#extended' => TRUE, > + // Add properties needed by value() and process() methods. > + '#field_name' => $this->fieldDefinition->getName(), > + // This is actually talking about the bundle ON WHICH this field is > + // placed, not the bundle(s) TO WHICH it can refer. In fact, it seems only > + // to be used, unnecessarily, on FileWidget::validateMultipleCount(). > + '#entity_type' => $items->getEntity()->getEntityTypeId(), > + '#display_field' => (bool) $field_settings['display_field'], > + '#display_default' => $field_settings['display_default'], > + '#description_field' => $field_settings['description_field'], > + '#cardinality' => $cardinality, > + ]; 946,968c375 < + /** < + * Constructs a WidgetBase object. < + * < + * @param string $plugin_id < + * The plugin_id for the widget. < + * @param mixed $plugin_definition < + * The plugin implementation definition. < + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition < + * The definition of the field to which the widget is associated. < + * @param array $settings < + * The widget settings. < + * @param array $third_party_settings < + * Any third party settings. < + * @param array $managed_file_info < + * An associative array describing the 'managed_file' element. < + * @param \Drupal\Core\Render\RendererInterface $renderer < + * The renderer. < + */ < + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, array $managed_file_info, RendererInterface $renderer) { < + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings); < + $this->managedFileInfo = $managed_file_info; < + $this->renderer = $renderer; < + } --- > + $element['#weight'] = $delta; 970,982c377,398 < + /** < + * {@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') < + ); --- > + // Save mid, the target_id value that this field ultimately cares about. > + if (!isset($items[$delta]->mids) && isset($items[$delta]->target_id)) { > + $items[$delta]->mids = [$items[$delta]->target_id]; > + } > + $element['#default_value'] = $items[$delta]->getValue() + $defaults; > + > + $default_fids = $element['#extended'] ? $element['#default_value']['fids'] : $element['#default_value']; > + if (empty($default_fids)) { > + $file_upload_help = [ > + '#theme' => 'file_upload_help', > + '#description' => $element['#description'], > + '#upload_validators' => $element['#upload_validators'], > + '#cardinality' => $cardinality, > + ]; > + $element['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help); > + $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE; > + if ($cardinality != 1 && $cardinality != -1) { > + $element['#element_validate'][] = [get_class($this), 'validateMultipleCount']; > + } > + } > + > + return $element; 988,992c404,417 < + public static function defaultSettings() { < + $settings = [ < + 'open' => TRUE, < + ]; < + return $settings + parent::defaultSettings(); --- > + public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { > + // Override FileWidget::massageFormValues() in order to reference Media > + // entities instead of files. > + $new_values = []; > + foreach ($values as &$value) { > + foreach ($value['mids'] as $mid) { > + $new_value = $value; > + $new_value['target_id'] = $mid; > + unset($new_value['mids']); > + $new_values[] = $new_value; > + } > + } > + > + return $new_values; 998,1001c423,424 < + public static function isApplicable(FieldDefinitionInterface $field_definition) { < + if ($field_definition->getType() !== 'entity_reference') { < + return FALSE; < + } --- > + public static function value($element, $input, FormStateInterface $form_state) { > + $return = parent::value($element, $input, $form_state); 1003,1004c426,449 < + if ($field_definition->getFieldStorageDefinition()->getSetting('target_type') !== 'media') { < + return FALSE; --- > + // When the form has just been submitted with file(s) being uploaded, > + // $input['mids'] should be empty. FileWidget::value() calls > + // ManagedFile::valueCallback(), which populates $return['fids']. If there's > + // info in there AND we're not doing the subsequent form rebuild, it's time > + // to make the media. > + if (empty($input['mids']) && !empty($return['fids']) && !$form_state->isRebuilding()) { > + foreach ($return['fids'] as $fid) { > + $file = File::load($fid); > + /** @var \Drupal\media\MediaInterface $media_entity */ > + $media_entity = Media::create([ > + 'name' => $file->getFilename(), > + 'bundle' => 'file', // TODO: generalize (note that we have to apply this to MediaInlineFileWidgetTest::setUp() too, in createdMediaType() call) > + 'uid' => \Drupal::currentUser()->id(), > + // TODO: figure out langcode in this context. > + // 'langcode' => !empty($element['#langcode']) ? $element['#langcode'] : LanguageInterface::LANGCODE_DEFAULT, > + ]); > + $media_entity->setPublished(); > + $source_field = $media_entity->getSource() > + ->getSourceFieldDefinition($media_entity->bundle->entity) > + ->getName(); > + $media_entity->set($source_field, $fid); > + $media_entity->save(); > + $return['mids'][] = $media_entity->id(); > + } 1006,1012c451,464 < + < + $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; --- > + else if (!empty($input['mids'])) { > + $mid = $input['mids']; > + $media_fields = 'media_' . $mid; > + > + if (!empty($input[$media_fields])) { > + $media_entity = Media::load($mid); > + foreach ($input[$media_fields] as $field_name => $field_value) { > + $media_entity->set($field_name, $field_value); > + } > + $media_entity->save(); > + } > + else { > + // Warn process() that this is a preexisting value. > + $return['media_already_saved'] = TRUE; 1013a466 > + $return['mids'] = [$input['mids']]; 1016c469 < + return FALSE; --- > + return $return; 1022,1038c475,478 < + 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'), < + ]; --- > + public static function process($element, FormStateInterface $form_state, $form) { > + $element = parent::process($element, $form_state, $form); > + $mid = FALSE; > + $show_form = FALSE; 1040c480,491 < + $cardinality = $field_storage_definition->getCardinality(); --- > + if (!empty($element['#value']['target_id'])) { > + // This is an existing entity ref and we're loading the edit page the > + // first time. Just show the icon and name. > + $mid = $element['#value']['target_id']; > + } > + else if (!empty($element['#value']['mids']) && count($element['#value']['mids']) === 1) { > + // If $element['mids']['#value'] has multiple values, that means this is > + // the first pass after someone uploaded several files; ignore. If it has > + // only ONE, then this media entity was just created and we want to force > + // the user to fill out all the new entity's required fields. I.e., show > + // the form, as well as the standard icon and name. > + $mid = $element['#value']['mids'][0]; 1042,1050c493,494 < + $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(); --- > + if (empty($element['#value']['media_already_saved'])) { > + $show_form = TRUE; 1053d496 < + $upload_validators = array_merge(...$upload_validators); 1055,1073c498,520 < + $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(), < + ]; --- > + if ($mid) { > + // First, get rid of the file link that FileManaged has created, and > + // replace with a media-entity link. > + $fid = $element['#value']['fids'][0]; > + unset($element['file_' . $fid]); > + // Under some circumstances this button said "remove selected," but since > + // we just removed the checkbox this button should instead always just say > + // "remove." > + $element['remove_button']['#value'] = t('Remove'); > + > + // Now show the media icon and name. > + $form_element_name = 'media_' . $mid; > + $form_element_parents = array_merge($element['#parents'], [$form_element_name]); > + $media_entity = Media::load($mid); > + > + $element[$form_element_name]['media_icon'] = [ > + '#theme' => 'image_style', > + '#style_name' => 'thumbnail', > + '#uri' => $media_entity->getSource()->getMetadata($media_entity, 'thumbnail_uri'), > + '#weight' => -20, > + ]; > + $link_ra = Link::createFromRoute($media_entity->getName(), 'entity.media.canonical', ['media' => $mid])->toRenderable(); > + $element[$form_element_name]['media_name'] = $link_ra + ['weight' => -10]; 1075,1078c522,532 < + // 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']; --- > + if ($show_form) { > + // Create and add the media entity form. > + $form_mode = 'add_inline'; > + $media_form = [ > + '#entity' => $media_entity, > + '#form_mode' => $form_mode, > + '#parents' => $form_element_parents, > + ]; > + $inline_form_handler = \Drupal::entityTypeManager() > + ->getHandler('media', $form_mode); > + $media_form = $inline_form_handler->entityForm($media_form, $form_state); 1080c534,536 < + $element['media_items']['#type'] = 'hidden'; --- > + $element[$form_element_name] += $media_form; > + } > + } 1086c542 < + * Adds this widget to the AJAX callback of the upload_button. --- > + * Form submission handler for upload/remove button of formElement(). 1088,1089c544,545 < + * @param array $element < + * A render array containing the upload button. --- > + * Overrides FileWidget::submit(), performing the exact same functionality, > + * only maintaining awareness of mids as well as fids. 1091,1092c547,548 < + * @return array < + * An updated render array. --- > + * @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit() > + * @see file_managed_file_submit() 1094,1104c550,569 < + 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 []; --- > + public static function submit($form, FormStateInterface $form_state) { > + // During the form rebuild, formElement() will create field item widget > + // elements using re-indexed deltas, so clear out FormState::$input to > + // avoid a mismatch between old and new deltas. The rebuilt elements will > + // have #default_value set appropriately for the current state of the field, > + // so nothing is lost in doing this. > + $button = $form_state->getTriggeringElement(); > + $parents = array_slice($button['#parents'], 0, -2); > + NestedArray::setValue($form_state->getUserInput(), $parents, NULL); > + > + // Go one level up in the form, to the widgets container. > + $element = NestedArray::getValue($form, array_slice($button['#array_parents'], 0, -1)); > + $field_name = $element['#field_name']; > + $parents = $element['#field_parents']; > + > + $submitted_values = NestedArray::getValue($form_state->getValues(), array_slice($button['#parents'], 0, -2)); > + foreach ($submitted_values as $delta => $submitted_value) { > + if (empty($submitted_value['fids'])) { > + unset($submitted_values[$delta]); > + } 1107,1143c572,583 < + 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; --- > + // If there are more files uploaded via the same widget, we have to separate > + // them, as we display each file in its own widget. > + $new_values = []; > + foreach ($submitted_values as $submitted_value) { > + if (is_array($submitted_value['fids']) && is_array($submitted_value['mids'])) { > + foreach ($submitted_value['fids'] as $delta => $fid) { > + $new_value = $submitted_value; > + $new_value['fids'] = [$fid]; > + $new_value['mids'] = [$submitted_value['mids'][$delta]]; > + $new_values[] = $new_value; > + } > + } 1146,1159c586,590 < + $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); --- > + // Re-index deltas after removing empty items. > + $submitted_values = array_values($new_values); > + > + // Update form_state values. > + NestedArray::setValue($form_state->getValues(), array_slice($button['#parents'], 0, -2), $submitted_values); 1161c592,595 < + return $response->addCommand(new OpenDialogUrlCommand($url)); --- > + // Update items. > + $field_state = static::getWidgetState($parents, $field_name, $form_state); > + $field_state['items'] = $submitted_values; > + static::setWidgetState($parents, $field_name, $form_state, $field_state); 1165c599,603 < diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php --- > diff --git a/core/modules/media/tests/fixtures/example_1.pdf b/core/modules/media/tests/fixtures/example_1.pdf > new file mode 100644 > index 0000000000..1bdb258c86 > Binary files /dev/null and b/core/modules/media/tests/fixtures/example_1.pdf differ > diff --git a/core/modules/media/tests/fixtures/example_2.pdf b/core/modules/media/tests/fixtures/example_2.pdf 1167c605,609 < index 0000000000..04c8a9a80a --- > index 0000000000..1bdb258c86 > Binary files /dev/null and b/core/modules/media/tests/fixtures/example_2.pdf differ > diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php > new file mode 100644 > index 0000000000..f35addbaa4 1169,1170c611,612 < +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaFileWidgetTest.php < @@ -0,0 +1,137 @@ --- > +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php > @@ -0,0 +1,99 @@ 1177c619 < +use Drupal\media\Entity\Media; --- > +use Drupal\Tests\field\Unit\FieldStorageConfigAccessControlHandlerTest; 1180,1181d621 < + * Tests the media_file field widget. < + * 1184c624 < +class MediaFileWidgetTest extends MediaJavascriptTestBase { --- > +class MediaInlineFileWidgetTest extends MediaJavascriptTestBase { 1187,1189c627 < + * A node content type. < + * < + * @var \Drupal\node\NodeTypeInterface --- > + * {@inheritdoc} 1191c629,633 < + protected $nodeType; --- > + public static $modules = ['node']; > + > + protected $strictConfigSchema = FALSE; > + > + protected $node_type; 1198,1204c640,645 < + < + $this->nodeType = $this->drupalCreateContentType(); < + < + $field_storage = FieldStorageConfig::create([ < + 'field_name' => 'field_media', < + 'entity_type' => 'node', < + 'type' => 'entity_reference', --- > + $media_type = $this->createMediaType(['bundle' => 'file'], 'file'); > + $media_required_field_storage = FieldStorageConfig::create([ > + 'type' => 'string', > + 'cardinality' => -1, > + 'entity_type' => 'media', > + 'field_name' => 'field_media_required', 1206c647 < + 'target_type' => 'media', --- > + 'max_length' => 255, 1208d648 < + 'cardinality' => 3, 1210,1244c650,654 < + $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', --- > + $media_required_field_storage->save(); > + $media_required_field = FieldConfig::create([ > + 'field_storage' => $media_required_field_storage, > + 'bundle' => $media_type->id(), > + 'required' => TRUE, 1246c656,659 < + $this->drupalLogin($account); --- > + $media_required_field->save(); > +// $media_form_display = entity_get_form_display('media', $media_type->id(), 'add_inline'); > +// $media_form_display->setComponent('field_media_required', ['type' => 'string_textfield']); > +// $media_form_display->save(); 1248,1332c661,662 < + $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 @@ < + + $node_type = $this->drupalCreateContentType(); > + $this->node_type = $node_type->id(); 1334,1335d663 < + 'entity_type' => 'user', < + 'field_name' => 'field_media', 1336a665,666 > + 'entity_type' => 'node', > + 'field_name' => 'field_media_reference', 1342d671 < + 1344d672 < + 'bundle' => 'user', 1345a674 > + 'bundle' => $node_type->id(), 1347c676 < + 'handler' => 'default:media', --- > + 'file_extensions' => 'pdf', 1349c678,680 < + 'target_bundles' => NULL, --- > + 'target_bundles' => [ > + $media_type->id(), > + ], 1353a685,688 > + $form_display = entity_get_form_display('node', $node_type->id(), 'default'); > + $form_display->setComponent('field_media_reference', ['type' => 'media_file']); > + $form_display->save(); > + } 1355,1364c690,708 < + $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'])); --- > + /** > + * {@inheritdoc} > + */ > + public function testInlineFileWidget() { > + $this->drupalGet('/node/add/' . $this->node_type); > + $assert = $this->assertSession(); > + $element = $assert->elementExists('css', 'input[type="file"]'); > + $filepath = \Drupal::root() . '/' . drupal_get_path('module', 'media') . '/tests/fixtures/example_1.pdf'; > + $this->getSession()->getPage()->attachFileToField($element->getAttribute('id'), $filepath); > + // Does the new media item's "name" field exist? > + $this->waitUntilVisible('input[value="example_1.pdf"]', 10000); > + // Does the other required field for this media type exist? > + $assert->elementExists('css', 'input[name*="field_media_required"]'); // fails > + // Can I upload a second file and get a second media item's "name" field? > + $element = $assert->elementExists('css', 'input[type="file"]'); // fails > + $filepath = \Drupal::root() . '/' . drupal_get_path('module', 'media') . '/tests/fixtures/example_2.pdf'; > + $this->getSession()->getPage()->attachFileToField($element->getAttribute('id'), $filepath); > + $this->waitUntilVisible('input[value="example_2.pdf"]', 10000); > + // Next: save the node and then follow a link to a working media entity.