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