Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modern fragments: video content elements #4823

Merged
merged 6 commits into from Jul 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions UPGRADE.md
Expand Up @@ -186,6 +186,8 @@ The following content element types have been rewritten as fragment controllers
- `toplink` (`ce_toplink` → `content_element/toplink`)
- `image` (`ce_image` → `content_element/image`)
- `gallery` (`ce_gallery` → `content_element/gallery`)
- `youtube` (`ce_youtube` → `content_element/youtube`)
- `vimeo` (`ce_vimeo` → `content_element/vimeo`)

The legacy content elements and their templates are still around and will only be dropped in Contao 6. If you want to
use them instead of the new ones, you can opt in on a per-element basis by adding the respective lines to your
Expand All @@ -203,6 +205,8 @@ $GLOBALS['TL_CTE']['links']['hyperlink'] = \Contao\ContentHyperlink::class;
$GLOBALS['TL_CTE']['links']['toplink'] = \Contao\ContentToplink::class;
$GLOBALS['TL_CTE']['media']['image'] = \Contao\ContentImage::class;
$GLOBALS['TL_CTE']['media']['gallery'] = \Contao\ContentGallery::class;
$GLOBALS['TL_CTE']['media']['youtube'] = \Contao\ContentYouTube::class;
$GLOBALS['TL_CTE']['media']['vimeo'] = \Contao\ContentVimeo::class;
```

### Show to guests only
Expand Down
158 changes: 158 additions & 0 deletions core-bundle/src/Controller/ContentElement/VideoController.php
@@ -0,0 +1,158 @@
<?php

declare(strict_types=1);

/*
* This file is part of Contao.
*
* (c) Leo Feyer
*
* @license LGPL-3.0-or-later
*/

namespace Contao\CoreBundle\Controller\ContentElement;

use Contao\ContentModel;
use Contao\CoreBundle\Image\Studio\Studio;
use Contao\CoreBundle\ServiceAnnotation\ContentElement;
use Contao\CoreBundle\Twig\FragmentTemplate;
use Contao\StringUtil;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* @ContentElement("vimeo", category="media")
* @ContentElement("youtube", category="media")
*
* @phpstan-type VideoSourceParameters array{
* provider: 'vimeo'|'youtube',
* video_id: string,
* options: array<string, string>,
* base_url: string,
* query: string,
* url: string
* }
*/
class VideoController extends AbstractContentElementController
{
public function __construct(private readonly Studio $studio)
{
}

protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
// Video source data, size and aspect ratio
$sourceParameters = match ($type = $template->get('type')) {
'vimeo' => $this->getVimeoSourceParameters($model),
'youtube' => $this->getYoutubeSourceParameters($model, $request->getLocale()),
default => throw new \InvalidArgumentException(sprintf('Unknown video provider "%s".', $type))
};

$template->set('source', $sourceParameters);

$size = StringUtil::deserialize($model->playerSize, true);

$template->set('width', $size[0] ?? 640);
$template->set('height', $size[1] ?? 360);
$template->set('aspect_ratio', $model->playerAspect);

// Meta data
$template->set('caption', $model->playerCaption);

// Splash image
$figure = !$model->splashImage ? null : $this->studio
->createFigureBuilder()
->fromUuid($model->singleSRC ?: '')
->setSize($model->size)
->buildIfResourceExists()
;

$template->set('splash_image', $figure);

return $template->getResponse();
}

/**
* @return array<string, string|array<string, string>>
*
* @phpstan-return VideoSourceParameters
*/
private function getVimeoSourceParameters(ContentModel $model): array
{
$options = [];

foreach (StringUtil::deserialize($model->vimeoOptions, true) as $option) {
[$option, $value] = match ($option) {
'vimeo_portrait', 'vimeo_title', 'vimeo_byline' => [substr($option, 6), '0'],
default => [substr($option, 6), '1'],
};

$options[$option] = $value;
}

if ($color = $model->playerColor) {
$options['color'] = $color;
}

$query = http_build_query($options);

if (($start = $model->playerStart) > 0) {
$options['start'] = $start;
$query .= "#t={$start}s";
}

return [
'provider' => 'vimeo',
'video_id' => $videoId = $model->vimeo,
'options' => $options,
'base_url' => $baseUrl = "https://player.vimeo.com/video/$videoId",
'query' => $query,
'url' => empty($query) ? $baseUrl : "$baseUrl?$query",
];
}

/**
* @return array<string, string|array<string, string>>
*
* @phpstan-return VideoSourceParameters
*/
private function getYoutubeSourceParameters(ContentModel $model, string $locale): array
{
$options = [];
$domain = 'https://www.youtube.com';

foreach (StringUtil::deserialize($model->youtubeOptions, true) as $option) {
if ('youtube_nocookie' === $option) {
$domain = 'https://www.youtube-nocookie.com';

continue;
}

[$option, $value] = match ($option) {
'youtube_fs', 'youtube_rel', 'youtube_controls' => [substr($option, 8), '0'],
'youtube_hl' => [substr($option, 8), \Locale::parseLocale($locale)[\Locale::LANG_TAG] ?? ''],
'youtube_iv_load_policy' => [substr($option, 8), '3'],
default => [substr($option, 8), '1'],
};

$options[$option] = $value;
}

if (($start = $model->playerStart) > 0) {
$options['start'] = $start;
}

if (($end = $model->playerStop) > 0) {
$options['end'] = $end;
}

return [
'provider' => 'youtube',
'video_id' => $videoId = $model->youtube,
'options' => $options,
'base_url' => $baseUrl = "$domain/embed/$videoId",
'query' => $query = http_build_query($options),
'url' => empty($query) ? $baseUrl : "$baseUrl?$query",
];
}
}
4 changes: 4 additions & 0 deletions core-bundle/src/Resources/config/controller.yaml
Expand Up @@ -90,6 +90,10 @@ services:

Contao\CoreBundle\Controller\ContentElement\UnfilteredHtmlController: ~

Contao\CoreBundle\Controller\ContentElement\VideoController:
arguments:
- '@contao.image.studio'

Contao\CoreBundle\Controller\FaviconController:
arguments:
- '@contao.framework'
Expand Down
4 changes: 0 additions & 4 deletions core-bundle/src/Resources/contao/config/config.php
Expand Up @@ -26,8 +26,6 @@
use Contao\ContentSliderStart;
use Contao\ContentSliderStop;
use Contao\ContentTeaser;
use Contao\ContentVimeo;
use Contao\ContentYouTube;
use Contao\CoreBundle\Controller\BackendCsvImportController;
use Contao\Crawl;
use Contao\FilesModel;
Expand Down Expand Up @@ -280,8 +278,6 @@
'media' => array
(
'player' => ContentPlayer::class,
'youtube' => ContentYouTube::class,
'vimeo' => ContentVimeo::class
),
'files' => array
(
Expand Down
6 changes: 6 additions & 0 deletions core-bundle/src/Resources/contao/languages/en/default.xlf
Expand Up @@ -2069,6 +2069,12 @@
<trans-unit id="MSC.manual">
<source>Manual</source>
</trans-unit>
<trans-unit id="MSC.splashScreen">
<source>Please click to load the video.</source>
</trans-unit>
<trans-unit id="MSC.dataTransmission">
<source>Your IP address will be transmitted to %s.</source>
</trans-unit>
<trans-unit id="UNITS.0">
<source>Byte</source>
</trans-unit>
Expand Down
@@ -0,0 +1,54 @@
{#
This component outputs a splash screen button. A client side script will
replace it with content from a template tag when it gets clicked.

<button data-splash>
<p>Click to load the iframe.</p>
<template>
<div class="my-content">
<iframe src="…"></iframe>
</div>
</template>
</button>

After activation the HTML of the above example will look like this:

<div class="my-content">
<iframe src="…"></iframe>
</div>

Optional variables:
@var \Contao\CoreBundle\String\HtmlAttributes splash_button_attributes
#}

{% trans_default_domain "contao_default" %}

{% block splash_screen_component %}
{% set splash_button_attributes = attrs(splash_button_attributes|default)
.set('data-splash-screen')
%}
<button{{ splash_button_attributes }}>
{% block splash_screen_button_content %}
{# Render your splash screen's button content here. #}
{{ ('MSC.splashScreen')|trans }}
{% endblock %}
<template>
{% block splash_screen_content %}
{# Render the actual content (present after activation) here. #}
{% endblock %}
</template>
</button>

{% block splash_screen_script %}
{% add "splash_screen_script" to body %}
<script>
document.querySelectorAll('*[data-splash-screen]').forEach(button => {
button.addEventListener('click', () => {
button.insertAdjacentHTML('afterend', button.querySelector('template').innerHTML);
button.remove();
})
})
</script>
{% endadd %}
{% endblock %}
{% endblock %}
@@ -0,0 +1,44 @@
{% trans_default_domain "contao_default" %}
{% extends "@Contao/content_element/_base.html.twig" %}
{% use "@Contao/component/_splash_screen.html.twig" %}
{% use "@Contao/component/_picture.html.twig" %}

{% block content %}
{% set video_attributes = attrs(video_attributes|default)
.addClass('aspect aspect--' ~ aspect_ratio, aspect_ratio)
%}
<figure{{ video_attributes }}>
{% if splash_image %}
{{ block('splash_screen_component') }}
{% else %}
{% block iframe %}
{% set iframe_attrs = attrs({width, height, src: source.url, allowfullscreen: true})
.mergeWith(iframe_attrs|default)
%}
<iframe{{ iframe_attrs }}></iframe>
{% endblock %}
{% endif %}

{% block video_caption %}
{% if caption %}
<figcaption{{ attrs(video_caption_attributes|default) }}>
{{- caption|insert_tag_raw -}}
</figcaption>
{% endif %}
{% endblock %}
</figure>
{% endblock %}

{% block splash_screen_button_content %}
{# Preview image #}
{% with {figure: splash_image} %}{{ block('picture_component') }}{% endwith %}

{# Textual note #}
{% block splash_screen_text %}
<p>{{ ('MSC.splashScreen')|trans }}</p>
{% endblock %}
{% endblock %}

{% block splash_screen_content %}
{{ block('iframe') }}
{% endblock %}
@@ -0,0 +1,6 @@
{% trans_default_domain "contao_default" %}
{% extends "@Contao/content_element/_video.html.twig" %}

{% block splash_screen_text %}
<p>{{ ('MSC.splashScreen')|trans }} {{ ('MSC.dataTransmission')|trans(['Vimeo']) }}</p>
{% endblock %}
@@ -0,0 +1,10 @@
{% trans_default_domain "contao_default" %}
{% extends "@Contao/content_element/_video.html.twig" %}

{% set iframe_attrs = attrs(iframe_attrs|default)
.set('allow', 'autoplay; encrypted-media; picture-in-picture; fullscreen')
%}

{% block splash_screen_text %}
<p>{{ ('MSC.splashScreen')|trans }} {{ ('MSC.dataTransmission')|trans(['YouTube']) }}</p>
{% endblock %}
Expand Up @@ -140,6 +140,7 @@ static function () use ($modelData): Metadata|null {

$controller->setFragmentOptions([
'template' => $template ?? "content_element/{$modelData['type']}",
'type' => $modelData['type'],
]);

$response = $controller(new Request(), $model, 'main');
Expand Down Expand Up @@ -335,6 +336,13 @@ protected function getDefaultInsertTagParser(): InsertTagParser
);

$insertTagParser = $this->createMock(InsertTagParser::class);

$replaceDemo = static fn (string $input): string => str_replace(
['{{demo}}', '{{br}}'],
['demo', '<br>'],
$input
);

$insertTagParser
->method('replace')
->willReturnCallback($replaceDemo)
Expand Down
Expand Up @@ -48,7 +48,7 @@ public function testOutputsUnorderedList(): void
new ListController(),
[
'type' => 'list',
'listitems' => serialize(['foo', 'bar']),
'listitems' => serialize(['foo', 'bar{{br}}baz <i>plain</i>']),
'listtype' => 'unordered',
'cssID' => serialize(['', 'my-class']),
],
Expand All @@ -58,7 +58,7 @@ public function testOutputsUnorderedList(): void
<div class="my-class content_element/list">
<ul>
<li>foo</li>
<li>bar</li>
<li>bar<br>baz &lt;i&gt;plain&lt;/i&gt;</li>
</ul>
</div>
HTML;
Expand Down