diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml index 5797ececdd..f94fbec950 100644 --- a/core/modules/file/config/schema/file.schema.yml +++ b/core/modules/file/config/schema/file.schema.yml @@ -70,6 +70,41 @@ field.field_settings.file: type: boolean label: 'Enable Description field' +file.formatter.media: + type: mapping + label: 'Media display format settings' + mapping: + controls: + type: boolean + label: 'Show playback controls' + autoplay: + type: boolean + label: 'Autoplay' + loop: + type: boolean + label: 'Loop' + multiple_file_display_type: + type: string + label: 'Display of multiple files' + +field.formatter.settings.file_audio: + type: file.formatter.media + label: 'Audio file display format settings' + +field.formatter.settings.file_video: + type: file.formatter.media + label: 'Video file display format settings' + mapping: + muted: + type: boolean + label: 'Muted' + width: + type: integer + label: 'Width' + height: + type: integer + label: 'Height' + field.formatter.settings.file_default: type: mapping label: 'Generic file format settings' diff --git a/core/modules/file/file.module b/core/modules/file/file.module index 1b55fe0e74..71ff20334b 100644 --- a/core/modules/file/file.module +++ b/core/modules/file/file.module @@ -571,6 +571,12 @@ function file_theme() { 'file_managed_file' => [ 'render element' => 'element', ], + 'file_audio' => [ + 'variables' => ['files' => [], 'attributes' => NULL], + ], + 'file_video' => [ + 'variables' => ['files' => [], 'attributes' => NULL], + ], // From file.field.inc. 'file_widget_multiple' => [ diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php new file mode 100644 index 0000000000..5fbb70c43d --- /dev/null +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php @@ -0,0 +1,26 @@ + TRUE, + 'autoplay' => FALSE, + 'loop' => FALSE, + 'multiple_file_display_type' => 'tags', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + return [ + 'controls' => [ + '#title' => $this->t('Show playback controls'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('controls'), + ], + 'autoplay' => [ + '#title' => $this->t('Autoplay'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('autoplay'), + ], + 'loop' => [ + '#title' => $this->t('Loop'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('loop'), + ], + 'multiple_file_display_type' => [ + '#title' => $this->t('Display of multiple files'), + '#type' => 'radios', + '#options' => [ + 'tags' => $this->t('Use multiple @tag tags, each with a single source.', ['@tag' => '<' . static::getMediaType() . '>']), + 'sources' => $this->t('Use multiple sources within a single @tag tag.', ['@tag' => '<' . static::getMediaType() . '>']), + ], + '#default_value' => $this->getSetting('multiple_file_display_type'), + ], + ]; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + if (!parent::isApplicable($field_definition)) { + return FALSE; + } + /** @var \Symfony\Component\HttpFoundation\File\MimeType\MimeTypeGuesserInterface $extension_mime_type_guesser */ + $extension_mime_type_guesser = \Drupal::service('file.mime_type.guesser.extension'); + $extension_list = array_filter(preg_split('/\s+/', $field_definition->getSetting('file_extensions'))); + + foreach ($extension_list as $extension) { + $mime_type = $extension_mime_type_guesser->guess('fakedFile.' . $extension); + + if (static::mimeTypeApplies($mime_type)) { + return TRUE; + } + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = []; + $summary[] = $this->t('Playback controls: %controls', ['%controls' => $this->getSetting('controls') ? $this->t('visible') : $this->t('hidden')]); + $summary[] = $this->t('Autoplay: %autoplay', ['%autoplay' => $this->getSetting('autoplay') ? $this->t('yes') : $this->t('no')]); + $summary[] = $this->t('Loop: %loop', ['%loop' => $this->getSetting('loop') ? $this->t('yes') : $this->t('no')]); + switch ($this->getSetting('multiple_file_display_type')) { + case 'tags': + $summary[] = $this->t('Multiple file display: Multiple HTML tags'); + break; + + case 'sources': + $summary[] = $this->t('Multiple file display: One HTML tag with multiple sources'); + break; + } + return $summary; + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + + $source_files = $this->getSourceFiles($items, $langcode); + if (empty($source_files)) { + return $elements; + } + + $attributes = $this->prepareAttributes(); + foreach ($source_files as $delta => $files) { + $elements[$delta] = [ + '#theme' => 'file_' . static::getMediaType(), + '#attributes' => $attributes, + '#files' => $files, + '#cache' => ['tags' => []], + ]; + + $cache_tags = []; + foreach ($files as $file) { + $cache_tags = Cache::mergeTags($cache_tags, $file['file']->getCacheTags()); + } + $elements[$delta]['#cache']['tags'] = $cache_tags; + } + + return $elements; + } + + /** + * Prepare the attributes according to the settings. + * + * @param string[] $additional_attributes + * Additional attributes to be applied to the HTML element. Attribute names + * will be used as key and value in the HTML element. + * + * @return \Drupal\Core\Template\Attribute + * Container with all the attributes for the HTML tag. + */ + protected function prepareAttributes(array $additional_attributes = []) { + $attributes = new Attribute(); + foreach (['controls', 'autoplay', 'loop'] + $additional_attributes as $attribute) { + if ($this->getSetting($attribute)) { + $attributes->setAttribute($attribute, $attribute); + } + } + return $attributes; + } + + /** + * Check if given MIME type applies to the media type of the formatter. + * + * @param string $mime_type + * The complete MIME type. + * + * @return bool + * TRUE if the MIME type applies, FALSE otherwise. + */ + protected static function mimeTypeApplies($mime_type) { + list($type) = explode('/', $mime_type, 2); + return ($type === static::getMediaType()); + } + + /** + * Gets source files with attributes. + * + * @param \Drupal\Core\Field\EntityReferenceFieldItemListInterface $items + * The item list. + * @param string $langcode + * The language code of the referenced entities to display. + * + * @return array + * Numerically indexed array, which again contains an associative array with + * the following key/values: + * - file => \Drupal\file\Entity\File + * - source_attributes => \Drupal\Core\Template\Attribute + */ + protected function getSourceFiles(EntityReferenceFieldItemListInterface $items, $langcode) { + $source_files = []; + // Because we can have the files grouped in a single media tag, we do a + // grouping in case the multiple file behavior is not 'tags'. + /** @var \Drupal\file\Entity\File $file */ + foreach ($this->getEntitiesToView($items, $langcode) as $file) { + if (static::mimeTypeApplies($file->getMimeType())) { + $source_attributes = new Attribute(); + $source_attributes + ->setAttribute('src', file_create_url($file->getFileUri())) + ->setAttribute('type', $file->getMimeType()); + if ($this->getSetting('multiple_file_display_type') === 'tags') { + $source_files[] = [ + [ + 'file' => $file, + 'source_attributes' => $source_attributes, + ], + ]; + } + else { + $source_files[0][] = [ + 'file' => $file, + 'source_attributes' => $source_attributes, + ]; + } + } + } + return $source_files; + } + +} diff --git a/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php new file mode 100644 index 0000000000..98f1de1d7f --- /dev/null +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php @@ -0,0 +1,26 @@ + FALSE, + 'width' => 640, + 'height' => 480, + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + return parent::settingsForm($form, $form_state) + [ + 'muted' => [ + '#title' => $this->t('Muted'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('muted'), + ], + 'width' => [ + '#type' => 'number', + '#title' => $this->t('Width'), + '#default_value' => $this->getSetting('width'), + '#size' => 5, + '#maxlength' => 5, + '#field_suffix' => $this->t('pixels'), + '#min' => 0, + '#required' => TRUE, + ], + 'height' => [ + '#type' => 'number', + '#title' => $this->t('Height'), + '#default_value' => $this->getSetting('height'), + '#size' => 5, + '#maxlength' => 5, + '#field_suffix' => $this->t('pixels'), + '#min' => 0, + '#required' => TRUE, + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + $summary[] = $this->t('Muted: %muted', ['%muted' => $this->getSetting('muted') ? $this->t('yes') : $this->t('no')]); + $summary[] = $this->t('Size: %width x %height pixels', [ + '%width' => $this->getSetting('width'), + '%height' => $this->getSetting('height'), + ]); + return $summary; + } + + /** + * {@inheritdoc} + */ + protected function prepareAttributes(array $additional_attributes = []) { + return parent::prepareAttributes(['muted']) + ->setAttribute('width', $this->getSetting('width')) + ->setAttribute('height', $this->getSetting('height')); + } + +} diff --git a/core/modules/file/templates/file-audio.html.twig b/core/modules/file/templates/file-audio.html.twig new file mode 100644 index 0000000000..c98725b99b --- /dev/null +++ b/core/modules/file/templates/file-audio.html.twig @@ -0,0 +1,22 @@ +{# +/** +* @file +* Default theme implementation to display the file entity as an audio tag. +* +* Available variables: +* - attributes: An array of HTML attributes, intended to be added to the +* audio tag. +* - files: And array of files to be added as sources for the audio tag. Each +* element is an array with the following elements: +* - file: The full file object. +* - source_attributes: An array of HTML attributes for to be added to the +* source tag. +* +* @ingroup themeable +*/ +#} + diff --git a/core/modules/file/templates/file-video.html.twig b/core/modules/file/templates/file-video.html.twig new file mode 100644 index 0000000000..162fd4933e --- /dev/null +++ b/core/modules/file/templates/file-video.html.twig @@ -0,0 +1,22 @@ +{# +/** +* @file +* Default theme implementation to display the file entity as a video tag. +* +* Available variables: +* - attributes: An array of HTML attributes, intended to be added to the +* video tag. +* - files: And array of files to be added as sources for the video tag. Each +* element is an array with the following elements: +* - file: The full file object. +* - source_attributes: An array of HTML attributes for to be added to the +* source tag. +* +* @ingroup themeable +*/ +#} + diff --git a/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php b/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php new file mode 100644 index 0000000000..a1e7b342cc --- /dev/null +++ b/core/modules/file/tests/src/Functional/Formatter/FileAudioFormatterTest.php @@ -0,0 +1,43 @@ +createMediaField('file_audio', 'mp3'); + + file_put_contents('public://file.mp3', str_repeat('t', 10)); + $file = File::create([ + 'uri' => 'public://file.mp3', + 'filename' => 'file.mp3', + ]); + $file->save(); + + $entity = EntityTest::create([ + $field_config->getName() => [ + [ + 'target_id' => $file->id(), + ], + ], + ]); + $entity->save(); + + $this->drupalGet($entity->toUrl()); + + $file_url = file_url_transform_relative(file_create_url($file->getFileUri())); + $this->assertSession()->elementExists('css', 'audio[controls="controls"]'); + $this->assertSession()->elementExists('css', "audio > source[src*='$file_url'][type='audio/mpeg']"); + } + +} diff --git a/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php b/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php new file mode 100644 index 0000000000..9d0511adc0 --- /dev/null +++ b/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php @@ -0,0 +1,75 @@ +drupalLogin($this->drupalCreateUser(['view test entity'])); + } + + /** + * Creates a file field and set's the correct formatter. + * + * @param string $formatter + * The formatter ID. + * @param string $file_extensions + * The file extensions of the new field. + * + * @return \Drupal\field\Entity\FieldConfig + * Newly created file field. + */ + protected function createMediaField($formatter, $file_extensions) { + $entity_type = $bundle = 'entity_test'; + $field_name = Unicode::strtolower($this->randomMachineName()); + + FieldStorageConfig::create([ + 'entity_type' => $entity_type, + 'field_name' => $field_name, + 'type' => 'file', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ])->save(); + $field_config = FieldConfig::create([ + 'entity_type' => $entity_type, + 'field_name' => $field_name, + 'bundle' => $bundle, + 'settings' => [ + 'file_extensions' => trim($file_extensions), + ], + ]); + $field_config->save(); + + $display = entity_get_display('entity_test', 'entity_test', 'full'); + $display->setComponent($field_name, [ + 'type' => $formatter, + 'settings' => [], + ])->save(); + + return $field_config; + } + +} diff --git a/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php b/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php new file mode 100644 index 0000000000..9f5b1a9dc7 --- /dev/null +++ b/core/modules/file/tests/src/Functional/Formatter/FileVideoFormatterTest.php @@ -0,0 +1,43 @@ +createMediaField('file_video', 'mp4'); + + file_put_contents('public://file.mp4', str_repeat('t', 10)); + $file = File::create([ + 'uri' => 'public://file.mp4', + 'filename' => 'file.mp4', + ]); + $file->save(); + + $entity = EntityTest::create([ + $field_config->getName() => [ + [ + 'target_id' => $file->id(), + ], + ], + ]); + $entity->save(); + + $this->drupalGet($entity->toUrl()); + + $file_url = file_url_transform_relative(file_create_url($file->getFileUri())); + $this->assertSession()->elementExists('css', 'video[controls="controls"]'); + $this->assertSession()->elementExists('css', "video > source[src*='$file_url'][type='video/mp4']"); + } + +} diff --git a/core/themes/classy/templates/field/file-audio.html.twig b/core/themes/classy/templates/field/file-audio.html.twig new file mode 100644 index 0000000000..f573e32d46 --- /dev/null +++ b/core/themes/classy/templates/field/file-audio.html.twig @@ -0,0 +1,23 @@ +{# +/** +* @file +* Default theme implementation to display the file entity as an audio tag. +* +* Available variables: +* - attributes: An array of HTML attributes, intended to be added to the +* audio tag. +* - files: And array of files to be added as sources for the audio tag. Each +* element is an array with the following elements: +* - file: The full file object. +* - source_attributes: An array of HTML attributes for to be added to the +* source tag. +* +* @ingroup themeable +*/ +#} +{{ attach_library('classy/file') }} + diff --git a/core/themes/classy/templates/field/file-video.html.twig b/core/themes/classy/templates/field/file-video.html.twig new file mode 100644 index 0000000000..88ab05bbc2 --- /dev/null +++ b/core/themes/classy/templates/field/file-video.html.twig @@ -0,0 +1,23 @@ +{# +/** +* @file +* Default theme implementation to display the file entity as a video tag. +* +* Available variables: +* - attributes: An array of HTML attributes, intended to be added to the +* video tag. +* - files: And array of files to be added as sources for the video tag. Each +* element is an array with the following elements: +* - file: The full file object. +* - source_attributes: An array of HTML attributes for to be added to the +* source tag. +* +* @ingroup themeable +*/ +#} +{{ attach_library('classy/file') }} + diff --git a/core/themes/stable/templates/field/file-audio.html.twig b/core/themes/stable/templates/field/file-audio.html.twig new file mode 100644 index 0000000000..c98725b99b --- /dev/null +++ b/core/themes/stable/templates/field/file-audio.html.twig @@ -0,0 +1,22 @@ +{# +/** +* @file +* Default theme implementation to display the file entity as an audio tag. +* +* Available variables: +* - attributes: An array of HTML attributes, intended to be added to the +* audio tag. +* - files: And array of files to be added as sources for the audio tag. Each +* element is an array with the following elements: +* - file: The full file object. +* - source_attributes: An array of HTML attributes for to be added to the +* source tag. +* +* @ingroup themeable +*/ +#} + diff --git a/core/themes/stable/templates/field/file-video.html.twig b/core/themes/stable/templates/field/file-video.html.twig new file mode 100644 index 0000000000..162fd4933e --- /dev/null +++ b/core/themes/stable/templates/field/file-video.html.twig @@ -0,0 +1,22 @@ +{# +/** +* @file +* Default theme implementation to display the file entity as a video tag. +* +* Available variables: +* - attributes: An array of HTML attributes, intended to be added to the +* video tag. +* - files: And array of files to be added as sources for the video tag. Each +* element is an array with the following elements: +* - file: The full file object. +* - source_attributes: An array of HTML attributes for to be added to the +* source tag. +* +* @ingroup themeable +*/ +#} +