diff --git a/core/modules/file/config/schema/file.schema.yml b/core/modules/file/config/schema/file.schema.yml index 5797ececdd..9a3fb207a8 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.rich_media: + type: mapping + label: 'Rich media display format settings' + mapping: + controls: + type: boolean + label: 'Show controls' + autoplay: + type: boolean + label: 'Autoplay' + loop: + type: boolean + label: 'Loop' + multiple_file_behavior: + type: string + label: 'Display of multiple files' + +field.formatter.settings.file_audio: + type: file.formatter.rich_media + label: 'Audio file display format settings' + +field.formatter.settings.file_video: + type: file.formatter.rich_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..4030643115 --- /dev/null +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileAudioFormatter.php @@ -0,0 +1,26 @@ +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['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('renderer') + ); + } + + /** + * {@inheritdoc} + */ + public static function defaultSettings() { + return [ + 'controls' => TRUE, + 'autoplay' => FALSE, + 'loop' => FALSE, + 'multiple_file_behavior' => 'tags', + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $element['controls'] = [ + '#title' => $this->t('Show audio controls'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('controls'), + ]; + $element['autoplay'] = [ + '#title' => $this->t('Autoplay'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('autoplay'), + ]; + $element['loop'] = [ + '#title' => $this->t('Loop'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('loop'), + ]; + $element['multiple_file_behavior'] = [ + '#title' => $this->t('Display of multiple files'), + '#type' => 'radios', + '#options' => [ + 'tags' => $this->t('Use multiple @tag tags, each with a single source.', ['@tag' => '<' . static::getMimeType() . '>']), + 'sources' => $this->t('Use multiple sources within a single @tag tag.', ['@tag' => '<' . static::getMimeType() . '>']), + ], + '#default_value' => $this->getSetting('multiple_file_behavior'), + ]; + + return $element; + } + + /** + * {@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('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')]); + $summary[] = $this->t('Multiple files: %multiple', ['%multiple' => $this->getSetting('multiple_file_behavior')]); + 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::getMimeType(), + '#attributes' => $attributes, + '#files' => $files, + ]; + foreach ($files as $file) { + $this->renderer->addCacheableDependency($elements[$delta], $file['file']); + } + } + + return $elements; + } + + /** + * Prepare the attributes according to the settings. + * + * @param array $additional_attributes + * Additional attributes for preparing the html tag attributes. + * + * @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 mimetype applies to formatter one. + * + * @param string $mimeType + * The full mimeType. + * + * @return bool + * Mimetype applies or not. + */ + protected static function mimeTypeApplies($mimeType) { + list($type) = explode('/', $mimeType, 2); + return ($type == static::getMimeType()); + } + + /** + * 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 + * List of files. + */ + protected function getSourceFiles(EntityReferenceFieldItemListInterface $items, $langcode) { + $source_files = []; + $multiple_file_behavior = $this->getSetting('multiple_file_behavior'); + + // 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'. + 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())); + $source_attributes->setAttribute('type', $file->getMimeType()); + if ($multiple_file_behavior == '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..386846c028 --- /dev/null +++ b/core/modules/file/src/Plugin/Field/FieldFormatter/FileMediaFormatterInterface.php @@ -0,0 +1,17 @@ + FALSE, + 'width' => NULL, + 'height' => NULL, + ] + parent::defaultSettings(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + + $element['muted'] = [ + '#title' => $this->t('Muted'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('muted'), + ]; + $element['width'] = [ + '#type' => 'textfield', + '#title' => $this->t('Width'), + '#default_value' => $this->getSetting('width'), + '#size' => 5, + '#maxlength' => 5, + '#field_suffix' => $this->t('pixels'), + ]; + $element['height'] = [ + '#type' => 'textfield', + '#title' => $this->t('Height'), + '#default_value' => $this->getSetting('height'), + '#size' => 5, + '#maxlength' => 5, + '#field_suffix' => $this->t('pixels'), + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + $summary[] = $this->t('Muted: %muted', ['%muted' => $this->getSetting('muted') ? $this->t('yes') : $this->t('no')]); + $width = $this->getSetting('width'); + $height = $this->getSetting('height'); + if ($width && $height) { + $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 = []) { + $video_attributes = parent::prepareAttributes(['muted']); + $width = $this->getSetting('width'); + $height = $this->getSetting('height'); + if ($width && $height) { + $video_attributes->setAttribute('width', $width); + $video_attributes->setAttribute('height', $height); + } + + return $video_attributes; + } + +} 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..ce7c4059b2 --- /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([ + $this->fieldName => [ + [ + 'target_id' => $file->id(), + ], + ], + ]); + $entity->save(); + + $this->drupalGet($entity->url()); + + $file_url = 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..2fbce8475d --- /dev/null +++ b/core/modules/file/tests/src/Functional/Formatter/FileMediaFormatterTestBase.php @@ -0,0 +1,78 @@ +drupalLogin($this->drupalCreateUser(['view test entity'])); + } + + protected function createMediaField($formatter, $file_extionsions) { + + $entityType = $bundle = 'entity_test'; + $this->fieldName = Unicode::strtolower($this->randomMachineName()); + + FieldStorageConfig::create(array( + 'entity_type' => $entityType, + 'field_name' => $this->fieldName, + 'type' => 'file', + 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, + ))->save(); + FieldConfig::create([ + 'entity_type' => $entityType, + 'field_name' => $this->fieldName, + 'bundle' => $bundle, + 'settings' => [ + 'file_extensions' => $file_extionsions, + ], + ])->save(); + + $this->display = entity_get_display('entity_test', 'entity_test', 'full'); + $this->display->setComponent($this->fieldName, [ + 'type' => $formatter, + 'settings' => [], + ]); + $this->display->save(); + } + +} 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..cb55890019 --- /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([ + $this->fieldName => [ + [ + 'target_id' => $file->id(), + ], + ], + ]); + $entity->save(); + + $this->drupalGet($entity->url()); + + $file_url = 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 +*/ +#} +