feat: page module previews for teasers, disrupter, disturber, map and lineup

The TYPO3 10 drawItem hooks become one preview renderer registered per
CType in TCA; it renders the old templates from a46918d with the same
variables (f:format.html is not allowed in the backend: f:sanitize.html).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
frommeandClaude Opus 5.5 committed 2026-10-24 12:56:22 +02:00
1 parent 44614a4a75
commit 0b17a24edd
13 files changed
+355

No files matched your search

@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace EP\EpTheme\Preview;
use TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\FileRepository;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\View\ViewFactoryData;
use TYPO3\CMS\Core\View\ViewFactoryInterface;
/**
* Page module previews of the theme's content elements, rendered from
* Templates/PreviewRenderer/<CType>.html as the TYPO3 10 drawItem hooks did
* (Classes/Hooks/PageLayoutView in a46918d), with the same template variables.
* Registered per CType in TCA (['types'][<CType>]['previewRenderer']).
*/
class ContentPreviewRenderer extends StandardContentPreviewRenderer
{
/**
* CType => [field holding the related uid, related table, template variable]
*/
protected const RELATED_RECORDS = [
'teaser_country' => ['tx_eptheme_context_country', 'tx_epproducts_domain_model_country', 'country'],
'teaser_region' => ['tx_eptheme_context_region', 'tx_epproducts_domain_model_region', 'region'],
'teaser_city' => ['tx_eptheme_context_city', 'tx_epproducts_domain_model_city', 'city'],
'teaser_hotel' => ['tx_eptheme_context_hotel', 'tx_epproducts_domain_model_hotel', 'hotel'],
'teaser_concept' => ['tx_eptheme_context_concept', 'tx_epproducts_domain_model_concept', 'concept'],
'teaser_product' => ['tx_eptheme_context_product', 'tx_epproducts_domain_model_product', 'product'],
'teaser_booking_link' => ['tx_eptheme_context_product', 'tx_epproducts_domain_model_product', 'product'],
];
public function renderPageModulePreviewContent(GridColumnItem $item): string
{
$row = $item->getRecord();
$cType = (string)$row['CType'];
$template = GeneralUtility::getFileAbsFileName(
'EXT:ep_theme/Resources/Private/Templates/PreviewRenderer/' . GeneralUtility::underscoredToUpperCamelCase($cType) . '.html'
);
if (!is_file($template)) {
return parent::renderPageModulePreviewContent($item);
}
$images = $this->getImages((int)$row['uid'], 'tt_content', 'image');
$variables = [
'data' => $row,
'flexForm' => GeneralUtility::makeInstance(FlexFormService::class)->convertFlexFormContentToArray((string)($row['pi_flexform'] ?? '')),
'firstImage' => $images[0] ?? null,
'images' => $images,
];
$variables = array_replace($variables, $this->getAssignmentsForTemplate($cType, $row));
$view = GeneralUtility::makeInstance(ViewFactoryInterface::class)->create(new ViewFactoryData(
templatePathAndFilename: $template,
request: $item->getContext()->getCurrentRequest(),
));
$view->assignMultiple($variables);
return $view->render();
}
protected function getAssignmentsForTemplate(string $cType, array $row): array
{
if (isset(self::RELATED_RECORDS[$cType])) {
[$field, $table, $name] = self::RELATED_RECORDS[$cType];
$uid = (int)($row[$field] ?? 0);
$assignments = [
'images' => $this->getImages($uid, $table, 'teaser_images'),
$name => $uid > 0 ? BackendUtility::getRecord($table, $uid) : null,
];
if ($cType === 'teaser_product') {
$hotelUid = (int)($row['tx_eptheme_context_hotel'] ?? 0);
$assignments['hotel'] = $hotelUid > 0 ? BackendUtility::getRecord('tx_epproducts_domain_model_hotel', $hotelUid) : null;
}
return $assignments;
}
switch ($cType) {
case 'gmap':
$regionUid = (int)($row['tx_eptheme_context_region'] ?? 0);
$content = $regionUid > 0
? BackendUtility::getRecord('tx_epproducts_domain_model_region', $regionUid)
: BackendUtility::getRecord('tx_epproducts_domain_model_hotel', (int)($row['tx_eptheme_context_hotel'] ?? 0));
return ['content' => $content];
case 'disrupter':
// tx_eptheme_disrupter is an MM relation; the field itself only holds the count
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_eptheme_ttcontent_tx_eptheme_disrupter_mm');
$disrupterUid = (int)$queryBuilder->select('uid_foreign')
->from('tx_eptheme_ttcontent_tx_eptheme_disrupter_mm')
->where($queryBuilder->expr()->eq('uid_local', $queryBuilder->createNamedParameter((int)$row['uid'])))
->orderBy('sorting')
->setMaxResults(1)
->executeQuery()
->fetchOne();
return ['content' => $disrupterUid > 0 ? BackendUtility::getRecord('tx_eptheme_domain_model_disrupter', $disrupterUid) : null];
case 'lineup':
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_eptheme_domain_model_lineup');
$rows = $queryBuilder->select('*')
->from('tx_eptheme_domain_model_lineup')
->where($queryBuilder->expr()->eq('tt_content', $queryBuilder->createNamedParameter((int)$row['uid'])))
->orderBy('sorting')
->executeQuery()
->fetchAllAssociative();
return ['rows' => $rows];
}
return [];
}
protected function getImages(int $uid, string $table, string $field): array
{
if ($uid <= 0) {
return [];
}
return GeneralUtility::makeInstance(FileRepository::class)->findByRelation($table, $field, $uid);
}
}
@@ -0,0 +1,13 @@
<?php
use EP\EpTheme\Preview\ContentPreviewRenderer;
defined('TYPO3') or die();
// Page module previews (Templates/PreviewRenderer/<CType>.html); loads after tt_content_element_*.php
foreach (['teaser_country', 'teaser_region', 'teaser_city', 'teaser_hotel', 'teaser_concept', 'teaser_product',
'teaser_booking_link', 'gmap', 'disturber', 'disrupter', 'lineup'] as $cType) {
if (isset($GLOBALS['TCA']['tt_content']['types'][$cType])) {
$GLOBALS['TCA']['tt_content']['types'][$cType]['previewRenderer'] = ContentPreviewRenderer::class;
}
}
@@ -0,0 +1,11 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Störer</strong>
</p>
<strong>
{content.headline}
</strong>
</div>
</div>
@@ -0,0 +1,24 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Störer</strong>
</p>
<strong>
<f:switch expression="{data.layout}">
<f:case value="1">
groß
</f:case>>
<f:case value="2">
Event klein
</f:case>
<f:case value="3">
Event groß
</f:case>
<f:defaultCase>
klein
</f:defaultCase>
</f:switch>
</strong>
</div>
</div>
@@ -0,0 +1,11 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>OSM Karte</strong>
</p>
<strong>
{content.name} ({content.latitude}, {content.longitude})
</strong>
</div>
</div>
@@ -0,0 +1,15 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Lineup</strong>
</p>
<ul>
<f:for each="{rows}" as="row">
<li>
{row.name}
</li>
</f:for>
</ul>
</div>
</div>
@@ -0,0 +1,23 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Teaser Buchungslink</strong>
</p>
<p>
<f:if condition="{images.0}">
<f:then>
<f:image image="{images.0.originalFile}" width="200" height="80c" />
</f:then>
<f:else>
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png" width="200" height="80c"/>
</f:else>
</f:if>
</p>
<strong>
{product.name}
</strong>
<br>
{product.teaser -> f:sanitize.html()}
</div>
</div>
@@ -0,0 +1,23 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Teaser Ort</strong>
</p>
<p>
<f:if condition="{images.0}">
<f:then>
<f:image image="{images.0.originalFile}" width="200" height="80c" />
</f:then>
<f:else>
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png" width="200" height="80c"/>
</f:else>
</f:if>
</p>
<strong>
{city.name}
</strong>
<br>
{city.teaser -> f:sanitize.html()}
</div>
</div>
@@ -0,0 +1,23 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Teaser Konzept</strong>
</p>
<p>
<f:if condition="{images.0}">
<f:then>
<f:image image="{images.0.originalFile}" width="200" height="80c" />
</f:then>
<f:else>
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png" width="200" height="80c"/>
</f:else>
</f:if>
</p>
<strong>
{concept.name}
</strong>
<br>
{concept.teaser -> f:sanitize.html()}
</div>
</div>
@@ -0,0 +1,23 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Teaser Land</strong>
</p>
<p>
<f:if condition="{images.0}">
<f:then>
<f:image image="{images.0.originalFile}" width="200" height="80c" />
</f:then>
<f:else>
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png" width="200" height="80c"/>
</f:else>
</f:if>
</p>
<strong>
{country.name}
</strong>
<br>
{country.teaser -> f:sanitize.html()}
</div>
</div>
@@ -0,0 +1,23 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Teaser Hotel</strong>
</p>
<p>
<f:if condition="{images.0}">
<f:then>
<f:image image="{images.0.originalFile}" width="200" height="80c" />
</f:then>
<f:else>
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png" width="200" height="80c"/>
</f:else>
</f:if>
</p>
<strong>
{hotel.name}
</strong>
<br>
{hotel.teaser -> f:sanitize.html()}
</div>
</div>
@@ -0,0 +1,23 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Teaser Produkt</strong>
</p>
<p>
<f:if condition="{images.0}">
<f:then>
<f:image image="{images.0.originalFile}" width="200" height="80c" />
</f:then>
<f:else>
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png" width="200" height="80c"/>
</f:else>
</f:if>
</p>
<strong>
{product.name}
</strong>
<br>
{product.teaser -> f:sanitize.html()}
</div>
</div>
@@ -0,0 +1,23 @@
{namespace be=TYPO3\CMS\Backend\ViewHelpers}
<div class="row">
<div class="col-xs-12">
<p>
<strong>Teaser Gebiet</strong>
</p>
<p>
<f:if condition="{images.0}">
<f:then>
<f:image image="{images.0.originalFile}" width="200" height="80c" />
</f:then>
<f:else>
<f:image src="EXT:ep_theme/Resources/Public/images/lazy.png" width="200" height="80c"/>
</f:else>
</f:if>
</p>
<strong>
{region.name}
</strong>
<br>
{region.teaser -> f:sanitize.html()}
</div>
</div>