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 new file mode 100644 index 0000000000..e082019a82 --- /dev/null +++ b/core/modules/media/config/optional/core.entity_form_mode.media.add_inline.yml @@ -0,0 +1,9 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media.add_inline +label: 'File Upload' +targetEntityType: media +cache: true 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 @@ +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); + + // 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, + ]; + + return $element; + } + +} diff --git a/core/modules/media/src/Entity/Media.php b/core/modules/media/src/Entity/Media.php index 49f7283f7e..2c6862f8a7 100644 --- a/core/modules/media/src/Entity/Media.php +++ b/core/modules/media/src/Entity/Media.php @@ -39,6 +39,7 @@ * "edit" = "Drupal\media\MediaForm", * "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm", * }, + * "add_inline" = "Drupal\media\Form\MediaInlineForm", * "translation" = "Drupal\content_translation\ContentTranslationHandler", * "views_data" = "Drupal\media\MediaViewsData", * "route_provider" = { diff --git a/core/modules/media/src/Form/MediaInlineForm.php b/core/modules/media/src/Form/MediaInlineForm.php new file mode 100644 index 0000000000..988de582a6 --- /dev/null +++ b/core/modules/media/src/Form/MediaInlineForm.php @@ -0,0 +1,154 @@ +entityFieldManager = $entity_field_manager; + $this->entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + $this->entityType = $entity_type; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $container->get('entity_field.manager'), + $container->get('entity_type.manager'), + $container->get('module_handler'), + $entity_type + ); + } + + /** + * {@inheritdoc} + */ + public function getEntityType() { + return $this->entityType; + } + + /** + * {@inheritdoc} + */ + 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'] = []; + } + + } + + // We've already set the just-uploaded file as the value of the source + // field, so hide that too. + $source_field = $entity->getSource() + ->getSourceFieldDefinition($entity->bundle->entity) + ->getName(); + $entity_form[$source_field]['#access'] = FALSE; + + // 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(); + } + } + } + + + return $entity_form; + } + + /** + * Gets the form display for the given entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity. + * @param string $form_mode + * The form mode. + * + * @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface + * The form display. + */ + protected function getFormDisplay(ContentEntityInterface $entity, $form_mode) { + return EntityFormDisplay::collectRenderDisplay($entity, $form_mode); + } + +} 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 @@ +getValues(). + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the parent form. + */ + public function entityForm(array $entity_form, FormStateInterface $form_state); + +} 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..7ac3eb3c10 --- /dev/null +++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php @@ -0,0 +1,310 @@ +getFieldSettings(); + + // 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, + ]; + + $element['#weight'] = $delta; + + // 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; + } + + /** + * {@inheritdoc} + */ + 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; + } + + /** + * {@inheritdoc} + */ + public static function value($element, $input, FormStateInterface $form_state) { + $return = parent::value($element, $input, $form_state); + + // 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(); + } + } + 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; + } + $return['mids'] = [$input['mids']]; + } + + return $return; + } + + /** + * {@inheritdoc} + */ + public static function process($element, FormStateInterface $form_state, $form) { + $element = parent::process($element, $form_state, $form); + $mid = FALSE; + $show_form = FALSE; + + 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]; + + if (empty($element['#value']['media_already_saved'])) { + $show_form = TRUE; + } + } + + 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]; + + 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); + + $element[$form_element_name] += $media_form; + } + } + + return $element; + } + + /** + * Form submission handler for upload/remove button of formElement(). + * + * Overrides FileWidget::submit(), performing the exact same functionality, + * only maintaining awareness of mids as well as fids. + * + * @see \Drupal\file\Plugin\Field\FieldWidget\FileWidget::submit() + * @see file_managed_file_submit() + */ + 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]); + } + } + + // 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; + } + } + } + + // 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); + + // 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); + } + +} 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 new file mode 100644 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 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php @@ -0,0 +1,99 @@ +createMediaType(['bundle' => 'file'], 'file'); + $media_required_field_storage = FieldStorageConfig::create([ + 'type' => 'string', + 'cardinality' => -1, + 'entity_type' => 'media', + 'field_name' => 'field_media_required', + 'settings' => [ + 'max_length' => 255, + ], + ]); + $media_required_field_storage->save(); + $media_required_field = FieldConfig::create([ + 'field_storage' => $media_required_field_storage, + 'bundle' => $media_type->id(), + 'required' => TRUE, + ]); + $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(); + + $node_type = $this->drupalCreateContentType(); + $this->node_type = $node_type->id(); + $field_storage = FieldStorageConfig::create([ + 'type' => 'entity_reference', + 'entity_type' => 'node', + 'field_name' => 'field_media_reference', + 'settings' => [ + 'target_type' => 'media', + ], + ]); + $field_storage->save(); + $field = FieldConfig::create([ + 'field_storage' => $field_storage, + 'bundle' => $node_type->id(), + 'settings' => [ + 'file_extensions' => 'pdf', + 'handler_settings' => [ + 'target_bundles' => [ + $media_type->id(), + ], + ], + ], + ]); + $field->save(); + $form_display = entity_get_form_display('node', $node_type->id(), 'default'); + $form_display->setComponent('field_media_reference', ['type' => 'media_file']); + $form_display->save(); + } + + /** + * {@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. + } + +}