Commit 8f300f53 authored by webchick's avatar webchick

Issue #1174892 by chr.fritsch, travist, Dave Reid, keithm, webchick,...

Issue #1174892 by chr.fritsch, travist, Dave Reid, keithm, webchick, phenaproxima, Everett Zufelt, slashrsm, Wim Leers, RobW, seanB, xjm, Gábor Hojtsy, larowlan, ericduran, webflo, Berdir, pschuelke: File field formatters for rich media display with <video> and <audio> HTML5 elements
parent f7c9dd93
......@@ -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'
......
......@@ -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' => [
......
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
/**
* Plugin implementation of the 'file_audio' formatter.
*
* @FieldFormatter(
* id = "file_audio",
* label = @Translation("Audio"),
* description = @Translation("Display the file using an HTML5 audio tag."),
* field_types = {
* "file"
* }
* )
*/
class FileAudioFormatter extends FileMediaFormatterBase {
/**
* {@inheritdoc}
*/
public static function getMediaType() {
return 'audio';
}
}
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Template\Attribute;
/**
* Base class for media file formatter.
*/
abstract class FileMediaFormatterBase extends FileFormatterBase implements FileMediaFormatterInterface {
/**
* Gets the HTML tag for the formatter.
*
* @return string
* The HTML tag of this formatter.
*/
protected function getHtmlTag() {
return static::getMediaType();
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'controls' => 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' => '<' . $this->getHtmlTag() . '>']),
'sources' => $this->t('Use multiple sources within a single @tag tag.', ['@tag' => '<' . $this->getHtmlTag() . '>']),
],
'#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' => $this->getPluginId(),
'#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_url_transform_relative(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;
}
}
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
/**
* Defines getter methods for FileMediaFormatterBase.
*
* This interface is used on the FileMediaFormatterBase class to ensure that
* each file media formatter will be based on a media type.
*
* Abstract classes are not able to implement abstract static methods,
* this interface will work around that.
*
* @see \Drupal\file\Plugin\Field\FieldFormatter\FileMediaFormatterBase
*/
interface FileMediaFormatterInterface {
/**
* Gets the applicable media type for a formatter.
*
* @return string
* The media type of this formatter.
*/
public static function getMediaType();
}
<?php
namespace Drupal\file\Plugin\Field\FieldFormatter;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'file_video' formatter.
*
* @FieldFormatter(
* id = "file_video",
* label = @Translation("Video"),
* description = @Translation("Display the file using an HTML5 video tag."),
* field_types = {
* "file"
* }
* )
*/
class FileVideoFormatter extends FileMediaFormatterBase {
/**
* {@inheritdoc}
*/
public static function getMediaType() {
return 'video';
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'muted' => 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'));
}
}
{#
/**
* @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
*/
#}
<audio {{ attributes }}>
{% for file in files %}
<source {{ file.source_attributes }} />
{% endfor %}
</audio>
{#
/**
* @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
*/
#}
<video {{ attributes }}>
{% for file in files %}
<source {{ file.source_attributes }} />
{% endfor %}
</video>
<?php
namespace Drupal\Tests\file\Functional\Formatter;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\file\Entity\File;
/**
* @coversDefaultClass \Drupal\file\Plugin\Field\FieldFormatter\FileAudioFormatter
* @group file
*/
class FileAudioFormatterTest extends FileMediaFormatterTestBase {
/**
* @covers ::viewElements
*
* @dataProvider dataProvider
*/
public function testRender($tag_count, $formatter_settings) {
$field_config = $this->createMediaField('file_audio', 'mp3', $formatter_settings);
file_put_contents('public://file.mp3', str_repeat('t', 10));
$file1 = File::create([
'uri' => 'public://file.mp3',
'filename' => 'file.mp3',
]);
$file1->save();
$file2 = File::create([
'uri' => 'public://file.mp3',
'filename' => 'file.mp3',
]);
$file2->save();
$entity = EntityTest::create([
$field_config->getName() => [
[
'target_id' => $file1->id(),
],
[
'target_id' => $file2->id(),
],
],
]);
$entity->save();
$this->drupalGet($entity->toUrl());
$file1_url = file_url_transform_relative(file_create_url($file1->getFileUri()));
$file2_url = file_url_transform_relative(file_create_url($file2->getFileUri()));
$assert_session = $this->assertSession();
$assert_session->elementsCount('css', 'audio[controls="controls"]', $tag_count);
$assert_session->elementExists('css', "audio > source[src='$file1_url'][type='audio/mpeg']");
$assert_session->elementExists('css', "audio > source[src='$file2_url'][type='audio/mpeg']");
}
}
<?php
namespace Drupal\Tests\file\Functional\Formatter;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\Tests\BrowserTestBase;
/**
* Provides methods specifically for testing File module's media formatter's.
*/
abstract class FileMediaFormatterTestBase extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'entity_test',
'field',
'file',
'user',
'system',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->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.
* @param array $formatter_settings
* Settings for the formatter.
*
* @return \Drupal\field\Entity\FieldConfig
* Newly created file field.
*/
protected function createMediaField($formatter, $file_extensions, array $formatter_settings = []) {
$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' => $formatter_settings,
])->save();
return $field_config;
}
/**
* Data provider for testRender.
*
* @return array
* An array of data arrays.
* The data array contains:
* - The number of expected HTML tags.
* - An array of settings for the field formatter.
*/
public function dataProvider() {
return [
[2, []],
[1, ['multiple_file_display_type' => 'sources']],
];
}
}
<?php
namespace Drupal\Tests\file\Functional\Formatter;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\file\Entity\File;
/**
* @coversDefaultClass \Drupal\file\Plugin\Field\FieldFormatter\FileVideoFormatter
* @group file
*/
class FileVideoFormatterTest extends FileMediaFormatterTestBase {
/**
* @covers ::viewElements
*
* @dataProvider dataProvider
*/
public function testRender($tag_count, $formatter_settings) {
$field_config = $this->createMediaField('file_video', 'mp4', $formatter_settings);
file_put_contents('public://file.mp4', str_repeat('t', 10));
$file1 = File::create([
'uri' => 'public://file.mp4',
'filename' => 'file.mp4',
]);
$file1->save();
$file2 = File::create([
'uri' => 'public://file.mp4',
'filename' => 'file.mp4',
]);
$file2->save();
$entity = EntityTest::create([
$field_config->getName() => [
[
'target_id' => $file1->id(),
],
[
'target_id' => $file2->id(),
],
],
]);
$entity->save();
$this->drupalGet($entity->toUrl());
$file1_url = file_url_transform_relative(file_create_url($file1->getFileUri()));
$file2_url = file_url_transform_relative(file_create_url($file2->getFileUri()));
$assert_session = $this->assertSession();
$assert_session->elementsCount('css', 'video[controls="controls"]', $tag_count);
$assert_session->elementExists('css', "video > source[src='$file1_url'][type='video/mp4']");
$assert_session->elementExists('css', "video > source[src='$file2_url'][type='video/mp4']");
}
}
{#
/**
* @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.