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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a modern content slider element #6626

Merged
merged 16 commits into from Dec 29, 2023
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
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -55,6 +55,7 @@
"contao-components/mootools": "^1.6.0.1",
"contao-components/simplemodal": "^3.0",
"contao-components/swipe": "^2.0.3",
"contao-components/swiper": "^11.0",
"contao-components/tablesort": "^4.0",
"contao-components/tablesorter": "^2.1",
"contao-components/tinymce4": "^5.0 || ^6.0",
Expand Down
1 change: 1 addition & 0 deletions core-bundle/composer.json
Expand Up @@ -55,6 +55,7 @@
"contao-components/mootools": "^1.6.0.1",
"contao-components/simplemodal": "^3.0",
"contao-components/swipe": "^2.0.3",
"contao-components/swiper": "^11.0",
"contao-components/tablesort": "^4.0",
"contao-components/tablesorter": "^2.1",
"contao-components/tinymce4": "^5.0 || ^6.0",
Expand Down
2 changes: 2 additions & 0 deletions core-bundle/config/controller.yaml
Expand Up @@ -108,6 +108,8 @@ services:
arguments:
- '@contao.filesystem.virtual.files'

Contao\CoreBundle\Controller\ContentElement\SwiperController: ~

Contao\CoreBundle\Controller\ContentElement\TableController: ~

Contao\CoreBundle\Controller\ContentElement\TemplateController: ~
Expand Down
5 changes: 1 addition & 4 deletions core-bundle/contao/config/config.php
Expand Up @@ -269,10 +269,7 @@
(
'accordionSingle' => ContentAccordion::class,
'accordionStart' => ContentAccordionStart::class,
'accordionStop' => ContentAccordionStop::class
),
'slider' => array
(
'accordionStop' => ContentAccordionStop::class,
'sliderStart' => ContentSliderStart::class,
'sliderStop' => ContentSliderStop::class
),
Expand Down
1 change: 1 addition & 0 deletions core-bundle/contao/dca/tl_content.php
Expand Up @@ -125,6 +125,7 @@
'accordionStart' => '{type_legend},type;{moo_legend},mooHeadline,mooStyle,mooClasses;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},cssID;{invisible_legend:hide},invisible,start,stop',
'accordionStop' => '{type_legend},type;{moo_legend},mooClasses;{template_legend:hide},customTpl;{protected_legend:hide},protected;{invisible_legend:hide},invisible,start,stop',
'accordionSingle' => '{type_legend},type;{moo_legend},mooHeadline,mooStyle,mooClasses;{text_legend},text;{image_legend},addImage;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},cssID;{invisible_legend:hide},invisible,start,stop',
'swiper' => '{type_legend},type,headline;{slider_legend},sliderDelay,sliderSpeed,sliderStartSlide,sliderContinuous;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},cssID;{invisible_legend:hide},invisible,start,stop',
'sliderStart' => '{type_legend},type,headline;{slider_legend},sliderDelay,sliderSpeed,sliderStartSlide,sliderContinuous;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},cssID;{invisible_legend:hide},invisible,start,stop',
'sliderStop' => '{type_legend},type;{template_legend:hide},customTpl;{protected_legend:hide},protected;{invisible_legend:hide},invisible,start,stop',
'code' => '{type_legend},type,headline;{text_legend},highlight,code;{template_legend:hide},customTpl;{protected_legend:hide},protected;{expert_legend:hide},cssID;{invisible_legend:hide},invisible,start,stop',
Expand Down
10 changes: 8 additions & 2 deletions core-bundle/contao/languages/en/default.xlf
Expand Up @@ -380,17 +380,23 @@
<trans-unit id="CTE.template.1">
<source>Generates an element to insert a custom template.</source>
</trans-unit>
<trans-unit id="CTE.swiper.0">
<source>Content slider</source>
</trans-unit>
<trans-unit id="CTE.swiper.1">
<source>Generates a content slider element.</source>
</trans-unit>
<trans-unit id="CTE.slider">
<source>Content slider</source>
</trans-unit>
<trans-unit id="CTE.sliderStart.0">
<source>Wrapper start</source>
<source>Slider wrapper start</source>
</trans-unit>
<trans-unit id="CTE.sliderStart.1">
<source>Generates the opening part of the slider wrapper.</source>
</trans-unit>
<trans-unit id="CTE.sliderStop.0">
<source>Wrapper stop</source>
<source>Slider wrapper stop</source>
</trans-unit>
<trans-unit id="CTE.sliderStop.1">
<source>Generates the closing part of the slider wrapper.</source>
Expand Down
Expand Up @@ -29,8 +29,8 @@
(function () {
new handorgel(document.querySelector('.content-accordion'), {
{% block init_options %}
// Put custom options here
multiSelectable: false,
{# Put custom options here #}
{% endblock %}
});
})();
Expand Down
78 changes: 78 additions & 0 deletions core-bundle/contao/templates/twig/content_element/swiper.html.twig
@@ -0,0 +1,78 @@
{% extends "@Contao/content_element/_base.html.twig" %}
{% use "@Contao/component/_stylesheet.html.twig" %}

{% set slider_attributes = attrs(slider_attributes|default)
.addClass('swiper')
.set('data-delay', delay)
.set('data-speed', speed)
.set('data-offset', offset)
.set('data-loop', loop)
%}

{% block content %}
<div{{ slider_attributes }}>
<div{{ attrs(slider_wrapper_attributes|default).addClass('swiper-wrapper') }}>
{% block slides %}
{% for element in nested_fragments %}
{% block slide %}
<div{{ attrs(slide_attributes|default).addClass('swiper-slide') }}>
{% block slide_inner %}
{{ content_element(element) }}
{% endblock %}
</div>
{% endblock %}
{% endfor %}
{% endblock %}
</div>
{% block controls %}
<button{{ attrs(button_prev_attributes|default).set('type', 'button').addClass('swiper-button-prev') }}></button>
<button{{ attrs(button_next_attributes|default).set('type', 'button').addClass('swiper-button-next') }}></button>
<div{{ attrs(pagination_attributes|default).addClass('swiper-pagination') }}></div>
{% endblock %}
</div>
{% endblock %}

{% block script %}
{% add "swiper_js" to body %}
<script src="{{ asset('js/swiper-bundle.min.js', 'contao-components/swiper') }}"></script>
<script>
(function() {
const swiper = document.querySelectorAll('.swiper');
swiper.forEach (el => {
let opts = {
speed: el.getAttribute('data-speed'),
initialSlide: el.getAttribute('data-offset'),
loop: el.hasAttribute('data-loop'),
};

let delay = el.getAttribute('data-delay');
if (delay > 0) {
opts['autoplay'] = { delay: delay };
}

new Swiper(el, Object.assign({
{% block init_options %}
pagination: {
el: '.swiper-pagination',
clickable: true,
},
navigation: {
prevEl: '.swiper-button-prev',
nextEl: '.swiper-button-next',
},
{# Put custom options here #}
{% endblock %}
}, opts));
});
})();
</script>
{% endadd %}
{% endblock %}

{% block style %}
{% set swiper_css_file = asset('css/swiper-bundle.min.css', 'contao-components/swiper') %}

{% add "swiper_css" to head %}
{% with {file: swiper_css_file} %}{{ block('stylesheet_component') }}{% endwith %}
{% endadd %}
{% endblock %}
33 changes: 33 additions & 0 deletions core-bundle/src/Controller/ContentElement/SwiperController.php
@@ -0,0 +1,33 @@
<?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\DependencyInjection\Attribute\AsContentElement;
use Contao\CoreBundle\Twig\FragmentTemplate;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

#[AsContentElement(category: 'miscellaneous', nestedFragments: true)]
class SwiperController extends AbstractContentElementController
{
protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
{
$template->set('delay', $model->sliderDelay);
$template->set('speed', $model->sliderSpeed);
$template->set('offset', $model->sliderStartSlide);
$template->set('loop', $model->sliderContinuous);

return $template->getResponse();
}
}
Expand Up @@ -15,21 +15,20 @@
use Contao\ContentModel;
use Contao\CoreBundle\Controller\ContentElement\AccordionController;
use Contao\CoreBundle\Fragment\Reference\ContentElementReference;
use Symfony\Component\HttpFoundation\Request;

class AccordionControllerTest extends ContentElementTestCase
{
public function testOutputsAccordion(): void
{
$model = $this->mockClassWithProperties(ContentModel::class, [
'id' => 1,
$text = $this->mockClassWithProperties(ContentModel::class, [
'type' => 'text',
'sectionHeadline' => 'Section',
'text' => '<p>Text.</p>',
'sectionHeadline' => 'Text',
]);

$request = new Request();
$request->attributes->set('nestedFragments', [new ContentElementReference($model, 'main', [], true)]);
$image = $this->mockClassWithProperties(ContentModel::class, [
'type' => 'image',
'sectionHeadline' => 'Image',
]);

$response = $this->renderWithModelData(
new AccordionController($this->getDefaultFramework()),
Expand All @@ -41,17 +40,28 @@ public function testOutputsAccordion(): void
false,
$responseContextData,
null,
$request,
[
new ContentElementReference($text, 'main', [], true),
new ContentElementReference($image, 'main', [], true),
],
);

$expectedOutput = <<<'HTML'
<div class="content-accordion">
<h3 class="handorgel__header">
<button class="handorgel__header__button">Section</button>
<button class="handorgel__header__button">Text</button>
</h3>
<div class="handorgel__content" data-open>
<div class="handorgel__content__inner">
Nested fragments
text
</div>
</div>
<h3 class="handorgel__header">
<button class="handorgel__header__button">Image</button>
</h3>
<div class="handorgel__content">
<div class="handorgel__content__inner">
image
</div>
</div>
</div>
Expand All @@ -64,15 +74,15 @@ public function testOutputsAccordion(): void

public function testDoesNotAddTheDataOpenAttribute(): void
{
$model = $this->mockClassWithProperties(ContentModel::class, [
'id' => 1,
$text = $this->mockClassWithProperties(ContentModel::class, [
'type' => 'text',
'sectionHeadline' => 'Section',
'text' => '<p>Text.</p>',
'sectionHeadline' => 'Text',
]);

$request = new Request();
$request->attributes->set('nestedFragments', [new ContentElementReference($model, 'main', [], true)]);
$image = $this->mockClassWithProperties(ContentModel::class, [
'type' => 'image',
'sectionHeadline' => 'Image',
]);

$response = $this->renderWithModelData(
new AccordionController($this->getDefaultFramework()),
Expand All @@ -84,17 +94,28 @@ public function testDoesNotAddTheDataOpenAttribute(): void
false,
$responseContextData,
null,
$request,
[
new ContentElementReference($text, 'main', [], true),
new ContentElementReference($image, 'main', [], true),
],
);

$expectedOutput = <<<'HTML'
<div class="content-accordion">
<h3 class="handorgel__header">
<button class="handorgel__header__button">Section</button>
<button class="handorgel__header__button">Text</button>
</h3>
<div class="handorgel__content">
<div class="handorgel__content__inner">
text
</div>
</div>
<h3 class="handorgel__header">
<button class="handorgel__header__button">Image</button>
</h3>
<div class="handorgel__content">
<div class="handorgel__content__inner">
Nested fragments
image
</div>
</div>
</div>
Expand Down
Expand Up @@ -108,11 +108,13 @@ protected function tearDown(): void
*
* @param-out array<string, array<int|string, string>> $responseContextData
*/
protected function renderWithModelData(AbstractContentElementController $controller, array $modelData, string|null $template = null, bool $asEditorView = false, array|null &$responseContextData = null, ContainerBuilder|null $adjustedContainer = null, Request|null $request = null): Response
protected function renderWithModelData(AbstractContentElementController $controller, array $modelData, string|null $template = null, bool $asEditorView = false, array|null &$responseContextData = null, ContainerBuilder|null $adjustedContainer = null, array $nestedFragments = []): Response
{
$framework = $this->getDefaultFramework($nestedFragments);

// Setup Twig environment
$loader = $this->getContaoFilesystemLoader();
$environment = $this->getEnvironment($loader);
$environment = $this->getEnvironment($loader, $framework);

// Setup container with helper services
$scopeMatcher = $this->createMock(ScopeMatcher::class);
Expand All @@ -128,7 +130,7 @@ protected function renderWithModelData(AbstractContentElementController $control
$container->set('contao.twig.filesystem_loader', $loader);
$container->set('contao.twig.interop.context_factory', new ContextFactory());
$container->set('twig', $environment);
$container->set('contao.framework', $this->getDefaultFramework());
$container->set('contao.framework', $framework);
$container->set('monolog.logger.contao.error', $this->createMock(LoggerInterface::class));
$container->set('fragment.handler', $this->createMock(FragmentHandler::class));

Expand Down Expand Up @@ -181,7 +183,10 @@ static function () use ($modelData): Metadata|null {
'type' => $modelData['type'],
]);

$response = $controller($request ?? new Request(), $model, 'main');
$request = new Request();
$request->attributes->set('nestedFragments', $nestedFragments);

$response = $controller($request, $model, 'main');

// Record response context data
$responseContextData = array_filter([
Expand Down Expand Up @@ -259,7 +264,7 @@ protected function getContaoFilesystemLoader(): ContaoFilesystemLoader
return $loader;
}

protected function getEnvironment(ContaoFilesystemLoader $contaoFilesystemLoader): Environment
protected function getEnvironment(ContaoFilesystemLoader $contaoFilesystemLoader, ContaoFramework $framework): Environment
{
$translator = $this->createMock(TranslatorInterface::class);
$translator
Expand Down Expand Up @@ -295,7 +300,6 @@ protected function getEnvironment(ContaoFilesystemLoader $contaoFilesystemLoader
// Runtime loaders
$insertTagParser = $this->getDefaultInsertTagParser();
$responseContextAccessor = $this->createMock(ResponseContextAccessor::class);
$framework = $this->getDefaultFramework();

$environment->addRuntimeLoader(
new FactoryRuntimeLoader([
Expand Down Expand Up @@ -432,7 +436,7 @@ static function (string $input) use ($replaceDemo): ChunkedText {
return $insertTagParser;
}

protected function getDefaultFramework(): ContaoFramework
protected function getDefaultFramework(array $nestedFragments = []): ContaoFramework
{
$GLOBALS['TL_LANG'] = [
'MSC' => [
Expand Down Expand Up @@ -497,7 +501,7 @@ protected function getDefaultFramework(): ContaoFramework
$controllerAdapter
->method('getContentElement')
->with($this->isInstanceOf(ContentElementReference::class))
->willReturn('Nested fragments')
->willReturnOnConsecutiveCalls(...array_map(static fn ($el) => $el->getContentModel()->type, $nestedFragments))
;

return $this->mockContaoFramework([
Expand Down