Compare commits

..
2 Commits
Author SHA1 Message Date
orasche 78af83d2e3 - The three market Content Blocks preview as compact collapsible rows:
thumbnail of the actual image (hero background, intro image, insights
  cover), key copy in the summary line, full details expanded; card and
  intro-paragraph previews are wrapped in the same mechanism
- New SubmarketPluginPreview for vitec_usecaselist / vitec_productfeature /
  vitec_productlist: instead of bare standard boxes they now list the
  selected story titles (flagging stories that still lack a quote), the
  featured product and the hand-picked products in order
- Container-style collapse for whole page-module COLUMNS: a chevron at the
  right of every column header folds a section down to its title row,
  state kept per page and colPos in localStorage - the slot view of a
  sub-market page folds to six slim bands
- Previews sit under the page module's pointer-events lock and its
  click-to-edit layer: pointer events are re-enabled inside the collapse
  markup and the toggle runs on pointerdown in the capture phase
- Two more v14 removals fixed on the way, both latent in the OLDER preview
  classes too (Usecaseshow, Modelcard would have crashed the page module):
  GridColumnItem::getRecord() now returns a Domain\Record object, and
  StandaloneView is gone - previews normalize the record via a shared
  helper and render through the ViewFactory
2026-10-02 15:00:05 +02:00
orasche 67be9de316 The embedded Page-layout image ended after the Products block - the Figma
frame 'Sub-Market Page' does not contain the Industry Insights band or the
footer, they live as siblings inside the surrounding section. The asset is
now rendered from the section (contentsOnly), so the customer sees the
complete page including Insights and footer; the section label rows fit
unchanged.
2026-10-02 13:50:51 +02:00
14 changed files with 465 additions and 23 deletions

No files matched your search

@@ -11,7 +11,6 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
/** /**
* Page-module preview for the VITEC Card plugin (vitec_modelcard): * Page-module preview for the VITEC Card plugin (vitec_modelcard):
@@ -45,7 +44,7 @@ class ModelcardPreviewRenderer extends StandardContentPreviewRenderer
public function renderPageModulePreviewContent(GridColumnItem $item): string public function renderPageModulePreviewContent(GridColumnItem $item): string
{ {
try { try {
$record = $item->getRecord(); $record = $this->recordArray($item);
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
$flexFormData = $flexFormService->convertFlexFormContentToArray($record['pi_flexform'] ?? ''); $flexFormData = $flexFormService->convertFlexFormContentToArray($record['pi_flexform'] ?? '');
@@ -77,8 +76,12 @@ class ModelcardPreviewRenderer extends StandardContentPreviewRenderer
} }
} }
$view = GeneralUtility::makeInstance(StandaloneView::class); // v14: StandaloneView was removed - views come from the ViewFactory.
$view->setTemplatePathAndFilename('EXT:vitec/Resources/Private/Templates/Preview/Modelcard.html'); $view = GeneralUtility::makeInstance(\TYPO3\CMS\Core\View\ViewFactoryInterface::class)
->create(new \TYPO3\CMS\Core\View\ViewFactoryData(
templatePathAndFilename: 'EXT:vitec/Resources/Private/Templates/Preview/Modelcard.html',
request: $GLOBALS['TYPO3_REQUEST'] ?? null,
));
$view->assignMultiple([ $view->assignMultiple([
'typeLabel' => self::TYPE_LABELS[$modelType] ?? $modelType, 'typeLabel' => self::TYPE_LABELS[$modelType] ?? $modelType,
'layout' => $layout, 'layout' => $layout,
@@ -124,4 +127,21 @@ class ModelcardPreviewRenderer extends StandardContentPreviewRenderer
} }
return ''; return '';
} }
/**
* v14: GridColumnItem::getRecord() returns a Domain\Record object, no
* longer an array (found 2026-10-02 - crashed the page module preview).
* Normalizes both shapes to the raw database array.
*
* @return array<string,mixed>
*/
private function recordArray(GridColumnItem $item): array
{
$record = $item->getRecord();
if ($record instanceof \TYPO3\CMS\Core\Domain\Record) {
$raw = $record->getRawRecord();
return $raw !== null ? $raw->toArray() : [];
}
return (array)$record;
}
} }
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace Evomedien\Vitec\Preview;
use TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Collapsible page-module previews for the three sub-market plugin sections
* (Olli's wish 2026-10-02: every section collapsible, content visible):
*
* vitec_usecaselist -> selected story titles (+ "no quote" flag)
* vitec_productfeature -> the featured product's title
* vitec_productlist -> hand-picked product titles / category info
*
* One class for all three - they only differ in how the FlexForm selection
* is resolved to titles.
*/
class SubmarketPluginPreview extends StandardContentPreviewRenderer
{
public function renderPageModulePreviewContent(GridColumnItem $item): string
{
$record = $this->recordArray($item);
$flexFormData = GeneralUtility::makeInstance(FlexFormService::class)
->convertFlexFormContentToArray((string)($record['pi_flexform'] ?? ''));
$settings = $flexFormData['settings'] ?? [];
$label = '';
$hint = '';
$items = [];
switch ((string)$record['CType']) {
case 'vitec_usecaselist':
$layout = (string)($settings['layout'] ?? 'grid');
$label = $layout === 'testimonials' ? 'Testimonials' : 'Success Stories';
$uids = GeneralUtility::intExplode(',', (string)($settings['usecases'] ?? ''), true);
foreach ($this->rows('tx_vitec_domain_model_usecase', $uids, ['quote']) as $row) {
$items[] = $row['title'] . (trim((string)$row['quote']) === '' ? ' — ⚠ kein Zitat' : '');
}
$hint = $uids === [] ? 'alle Stories (' . $layout . ')' : count($items) . ' ausgewählt · ' . $layout;
break;
case 'vitec_productfeature':
$label = 'Featured Product';
$uid = (int)($settings['product'] ?? 0);
foreach ($this->rows('tx_vitec_domain_model_product', $uid > 0 ? [$uid] : []) as $row) {
$items[] = $row['title'];
}
$hint = $items[0] ?? '⚠ kein Produkt gewählt';
break;
case 'vitec_productlist':
$label = 'Products';
$uids = GeneralUtility::intExplode(',', (string)($settings['selectedproducts'] ?? ''), true);
if ($uids !== []) {
foreach ($this->rows('tx_vitec_domain_model_product', $uids) as $row) {
$items[] = $row['title'];
}
$hint = count($items) . ' handverlesen';
} else {
$hint = !empty($settings['allproducts']) ? 'alle Produkte' : 'nach Kategorie';
}
break;
}
// v14: StandaloneView was removed - views come from the ViewFactory.
$view = GeneralUtility::makeInstance(\TYPO3\CMS\Core\View\ViewFactoryInterface::class)
->create(new \TYPO3\CMS\Core\View\ViewFactoryData(
templatePathAndFilename: 'EXT:vitec/Resources/Private/Templates/Preview/SubmarketSection.html',
request: $GLOBALS['TYPO3_REQUEST'] ?? null,
));
$view->assignMultiple([
'label' => $label,
'hint' => $hint,
'items' => $items,
'header' => (string)($record['header'] ?? ''),
]);
return $view->render();
}
/**
* Title rows for the given uids, in the given order.
*
* @param int[] $uids
* @param string[] $extraFields
* @return array<int,array<string,mixed>>
*/
private function rows(string $table, array $uids, array $extraFields = []): array
{
if ($uids === []) {
return [];
}
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
$rows = $queryBuilder->select('uid', 'title', ...$extraFields)->from($table)
->where(
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($uids, Connection::PARAM_INT_ARRAY)),
$queryBuilder->expr()->eq('deleted', 0)
)
->executeQuery()->fetchAllAssociative();
$byUid = array_column($rows, null, 'uid');
$ordered = [];
foreach ($uids as $uid) {
if (isset($byUid[$uid])) {
$ordered[] = $byUid[$uid];
}
}
return $ordered;
}
/**
* v14: GridColumnItem::getRecord() returns a Domain\Record object, no
* longer an array (found 2026-10-02 - crashed the page module preview).
* Normalizes both shapes to the raw database array.
*
* @return array<string,mixed>
*/
private function recordArray(GridColumnItem $item): array
{
$record = $item->getRecord();
if ($record instanceof \TYPO3\CMS\Core\Domain\Record) {
$raw = $record->getRawRecord();
return $raw !== null ? $raw->toArray() : [];
}
return (array)$record;
}
}
@@ -8,7 +8,6 @@ use TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer;
use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem; use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem;
use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView;
use Evomedien\Vitec\Domain\Repository\UsecaseRepository; use Evomedien\Vitec\Domain\Repository\UsecaseRepository;
/** /**
@@ -22,10 +21,14 @@ class UsecaseshowPluginPreview extends StandardContentPreviewRenderer
*/ */
public function renderPageModulePreviewContent(GridColumnItem $item): string public function renderPageModulePreviewContent(GridColumnItem $item): string
{ {
$record = $item->getRecord(); $record = $this->recordArray($item);
$view = GeneralUtility::makeInstance(StandaloneView::class); // v14: StandaloneView was removed - views come from the ViewFactory.
$view->setTemplatePathAndFilename('EXT:vitec/Resources/Private/Templates/Preview/Usecaseshow.html'); $view = GeneralUtility::makeInstance(\TYPO3\CMS\Core\View\ViewFactoryInterface::class)
->create(new \TYPO3\CMS\Core\View\ViewFactoryData(
templatePathAndFilename: 'EXT:vitec/Resources/Private/Templates/Preview/Usecaseshow.html',
request: $GLOBALS['TYPO3_REQUEST'] ?? null,
));
// Parse FlexForm data // Parse FlexForm data
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
@@ -69,7 +72,7 @@ class UsecaseshowPluginPreview extends StandardContentPreviewRenderer
*/ */
public function renderPageModulePreviewHeader(GridColumnItem $item): string public function renderPageModulePreviewHeader(GridColumnItem $item): string
{ {
$record = $item->getRecord(); $record = $this->recordArray($item);
// Parse FlexForm to get usecase info for header // Parse FlexForm to get usecase info for header
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
@@ -83,4 +86,21 @@ class UsecaseshowPluginPreview extends StandardContentPreviewRenderer
return '<strong>🎯 Usecase Show Plugin</strong> <small style="color: orange;">(No usecase selected)</small>'; return '<strong>🎯 Usecase Show Plugin</strong> <small style="color: orange;">(No usecase selected)</small>';
} }
/**
* v14: GridColumnItem::getRecord() returns a Domain\Record object, no
* longer an array (found 2026-10-02 - crashed the page module preview).
* Normalizes both shapes to the raw database array.
*
* @return array<string,mixed>
*/
private function recordArray(GridColumnItem $item): array
{
$record = $item->getRecord();
if ($record instanceof \TYPO3\CMS\Core\Domain\Record) {
$raw = $record->getRawRecord();
return $raw !== null ? $raw->toArray() : [];
}
return (array)$record;
}
} }
@@ -80,6 +80,13 @@ use TYPO3\CMS\Extbase\Utility\ExtensionUtility;
'One card for a Product, Success Story, Market or Solution — all data from the model.', 'One card for a Product, Success Story, Market or Solution — all data from the model.',
'FILE:EXT:vitec/Configuration/FlexForms/Modelcard.xml' 'FILE:EXT:vitec/Configuration/FlexForms/Modelcard.xml'
); );
// Collapsible sub-market section previews (2026-10-02)
$GLOBALS['TCA']['tt_content']['types']['vitec_usecaselist']['previewRenderer']
= \Evomedien\Vitec\Preview\SubmarketPluginPreview::class;
$GLOBALS['TCA']['tt_content']['types']['vitec_productfeature']['previewRenderer']
= \Evomedien\Vitec\Preview\SubmarketPluginPreview::class;
$GLOBALS['TCA']['tt_content']['types']['vitec_productlist']['previewRenderer']
= \Evomedien\Vitec\Preview\SubmarketPluginPreview::class;
$GLOBALS['TCA']['tt_content']['types']['vitec_modelcard']['previewRenderer'] $GLOBALS['TCA']['tt_content']['types']['vitec_modelcard']['previewRenderer']
= \Evomedien\Vitec\Preview\ModelcardPreviewRenderer::class; = \Evomedien\Vitec\Preview\ModelcardPreviewRenderer::class;
@@ -3,7 +3,16 @@
<f:section name="Content"> <f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/> <f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<f:asset.script identifier="vitec-backend-collapse" src="EXT:vitec/Resources/Public/JavaScript/backend-preview-collapse.js"/>
<details class="vitec-collapse">
<summary>
<f:if condition="{data.image.0}"><f:image image="{data.image.0}" class="vitec-collapse__thumb" width="72c" height="44c" alt=""/></f:if>
<span class="vitec-collapse__label">Card</span>
<span class="vitec-collapse__hint">{data.header}</span>
<f:if condition="{data.layout_variant} == 'flipcard'"><span class="vitec-badge">Flip</span></f:if>
</summary>
<div class="vitec-collapse__body">
<div class="vitec-preview vitec-preview--{data.background_variant}"> <div class="vitec-preview vitec-preview--{data.background_variant}">
<f:comment>Thumbnail: image > icon > placeholder</f:comment> <f:comment>Thumbnail: image > icon > placeholder</f:comment>
@@ -151,6 +160,8 @@
</div> </div>
</div> </div>
</div>
</details>
</f:section> </f:section>
</html> </html>
@@ -3,7 +3,14 @@
<f:section name="Content"> <f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/> <f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<f:asset.script identifier="vitec-backend-collapse" src="EXT:vitec/Resources/Public/JavaScript/backend-preview-collapse.js"/>
<details class="vitec-collapse">
<summary>
<span class="vitec-collapse__label">Intro Paragraph</span>
<span class="vitec-collapse__hint">{data.header}</span>
</summary>
<div class="vitec-collapse__body">
<div class="vitec-preview"> <div class="vitec-preview">
<div class="vitec-preview__body"> <div class="vitec-preview__body">
<div class="vitec-preview__label">VITEC · Intro Paragraph</div> <div class="vitec-preview__label">VITEC · Intro Paragraph</div>
@@ -83,6 +90,8 @@
</div> </div>
</div> </div>
</div>
</details>
</f:section> </f:section>
</html> </html>
@@ -1,12 +1,31 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> <html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Preview"/> <f:layout name="Preview"/>
<f:section name="Content"> <f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/> <f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<div class="vitec-preview"> <f:asset.script identifier="vitec-backend-collapse" src="EXT:vitec/Resources/Public/JavaScript/backend-preview-collapse.js"/>
<div class="vitec-preview__body">
<strong>Market Hero</strong> — {data.vitec_subline} <details class="vitec-collapse">
<f:if condition="{data.vitec_video_url}"><span> · 🎬 {data.vitec_video_title}</span></f:if> <summary>
<f:if condition="{data.bgimage.0}">
<f:image image="{data.bgimage.0}" class="vitec-collapse__thumb" width="72c" height="44c" alt=""/>
</f:if>
<span class="vitec-collapse__label">1 · Market Hero</span>
<span class="vitec-collapse__hint"><f:format.crop maxCharacters="80">{data.subline}</f:format.crop></span>
<f:if condition="{data.video_url}"><span class="vitec-badge">🎬 Video</span></f:if>
</summary>
<div class="vitec-collapse__body">
<p><strong>Subline:</strong> {data.subline}</p>
<p><strong>CTA:</strong> <f:if condition="{data.cta_label}"><f:then>{data.cta_label}</f:then><f:else>Get in touch (Default)</f:else></f:if></p>
<f:if condition="{data.video_url}">
<p><strong>Video:</strong> {data.video_title} — {data.video_url}</p>
</f:if>
<f:if condition="{data.bgimage.0}">
<f:then><f:image image="{data.bgimage.0}" width="280c" alt=""/></f:then>
<f:else><p><em>Kein Hintergrundbild — Fallback: dunkler Verlauf.</em></p></f:else>
</f:if>
</div> </div>
</div> </details>
</f:section> </f:section>
</html> </html>
@@ -1,12 +1,28 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> <html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Preview"/> <f:layout name="Preview"/>
<f:section name="Content"> <f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/> <f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<div class="vitec-preview"> <f:asset.script identifier="vitec-backend-collapse" src="EXT:vitec/Resources/Public/JavaScript/backend-preview-collapse.js"/>
<div class="vitec-preview__body">
<strong>Market Insights</strong> — <f:format.crop maxCharacters="100">{data.vitec_text1}</f:format.crop> <details class="vitec-collapse">
<f:if condition="{data.vitec_video_url}"><span> · 🎬 {data.vitec_video_person}</span></f:if> <summary>
<f:if condition="{data.video_cover.0}">
<f:image image="{data.video_cover.0}" class="vitec-collapse__thumb" width="72c" height="44c" alt=""/>
</f:if>
<span class="vitec-collapse__label">6 · Industry Insights</span>
<span class="vitec-collapse__hint"><f:format.crop maxCharacters="80">{data.text1}</f:format.crop></span>
<f:if condition="{data.video_url}"><span class="vitec-badge">🎬 {data.video_person}</span></f:if>
</summary>
<div class="vitec-collapse__body">
<p>{data.text1}</p>
<p>{data.text2}</p>
<p><strong>CTA:</strong> <f:if condition="{data.cta_label}"><f:then>{data.cta_label} → <f:if condition="{data.cta_target}"><f:then>{data.cta_target}</f:then><f:else>/success-stories</f:else></f:if></f:then><f:else>kein Button</f:else></f:if></p>
<f:if condition="{data.video_url}">
<p><strong>Video:</strong> {data.video_title} — {data.video_person}, {data.video_role}</p>
</f:if>
</div> </div>
</div> </details>
</f:section> </f:section>
</html> </html>
@@ -1,11 +1,29 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true"> <html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:layout name="Preview"/> <f:layout name="Preview"/>
<f:section name="Content"> <f:section name="Content">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/> <f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<div class="vitec-preview"> <f:asset.script identifier="vitec-backend-collapse" src="EXT:vitec/Resources/Public/JavaScript/backend-preview-collapse.js"/>
<div class="vitec-preview__body">
<strong>Market Intro</strong> — <f:format.crop maxCharacters="120">{data.vitec_intro_text}</f:format.crop> <details class="vitec-collapse">
<summary>
<f:if condition="{data.image.0}">
<f:image image="{data.image.0}" class="vitec-collapse__thumb" width="72c" height="44c" alt=""/>
</f:if>
<span class="vitec-collapse__label">2 · Market Intro</span>
<span class="vitec-collapse__hint"><f:format.crop maxCharacters="90">{data.intro_text}</f:format.crop></span>
</summary>
<div class="vitec-collapse__body">
<p>{data.intro_text}</p>
<p>
<strong>Learn more:</strong> <f:if condition="{data.learnmore_target}"><f:then>{data.learnmore_label} → {data.learnmore_target}</f:then><f:else>kein Button</f:else></f:if>
· <strong>Product Portfolio:</strong> <f:if condition="{data.show_portfolio}"><f:then>sichtbar</f:then><f:else>ausgeblendet</f:else></f:if>
</p>
<f:if condition="{data.image.0}">
<f:image image="{data.image.0}" width="280c" alt=""/>
</f:if>
</div> </div>
</div> </details>
</f:section> </f:section>
</html> </html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 273 KiB

After

Width:  |  Height:  |  Size: 297 KiB

@@ -0,0 +1,30 @@
<html xmlns:f="http://typo3.org/ns/TYPO3/CMS/Fluid/ViewHelpers" data-namespace-typo3-fluid="true">
<f:asset.css identifier="vitec-backend-preview" href="EXT:vitec/Resources/Public/Css/backend-preview.css"/>
<f:asset.script identifier="vitec-backend-collapse" src="EXT:vitec/Resources/Public/JavaScript/backend-preview-collapse.js"/>
<details class="vitec-collapse">
<summary>
<span class="vitec-collapse__label">{label}</span>
<span class="vitec-collapse__hint">{hint}</span>
</summary>
<div class="vitec-collapse__body">
<f:if condition="{header}">
<p><strong>Überschrift:</strong> {header}</p>
</f:if>
<f:if condition="{items}">
<f:then>
<ol style="margin:0.25rem 0 0 1rem; padding:0;">
<f:for each="{items}" as="entry">
<li>{entry}</li>
</f:for>
</ol>
</f:then>
<f:else>
<p><em>Keine Einzelauswahl.</em></p>
</f:else>
</f:if>
</div>
</details>
</html>
@@ -245,3 +245,86 @@
display: none; display: none;
} }
} }
/* =============================================================
Collapsible section previews (sub-market template, 2026-10-02)
Same practical effect as the container collapse on Two Columns,
built with <details> so it works for ANY element preview.
============================================================= */
details.vitec-collapse {
border-left: 4px solid #26358C;
background: var(--typo3-state-default-bg, #f8f9fa);
border-radius: 4px;
}
details.vitec-collapse > summary {
cursor: pointer;
padding: 0.6rem 0.75rem;
display: flex;
gap: 0.75rem;
align-items: center;
list-style: none;
font-size: 0.875rem;
}
details.vitec-collapse > summary::-webkit-details-marker { display: none; }
details.vitec-collapse > summary::before {
content: '▸';
color: #F47937;
transition: transform 0.15s ease;
flex: 0 0 auto;
}
details.vitec-collapse[open] > summary::before { transform: rotate(90deg); }
.vitec-collapse__label {
font-weight: 700;
color: #26358C;
white-space: nowrap;
}
.vitec-collapse__hint {
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vitec-collapse__thumb {
width: 72px;
height: 44px;
object-fit: cover;
border-radius: 3px;
flex: 0 0 auto;
}
.vitec-collapse__body {
padding: 0 0.75rem 0.75rem 2.1rem;
font-size: 0.875rem;
}
.vitec-collapse__body p { margin: 0.25rem 0; }
.vitec-collapse__body img { max-width: 280px; border-radius: 3px; margin-top: 0.4rem; }
/* The page module disables pointer events on preview content so clicks hit
the element row - re-enable them inside our collapsible previews, or the
summary can never be toggled (found 2026-10-02). */
details.vitec-collapse,
details.vitec-collapse * {
pointer-events: auto;
}
details.vitec-collapse > summary { user-select: none; }
/* =============================================================
Collapsible page-module COLUMNS (sub-market sections etc.),
container-style: chevron button at the right of the column
header, state kept per page+column in localStorage.
============================================================= */
.t3-page-column-header { position: relative; }
button.vitec-col-toggle {
position: absolute;
top: 50%;
right: 0.5rem;
transform: translateY(-50%);
border: 0;
background: transparent;
cursor: pointer;
font-size: 1rem;
line-height: 1;
color: #F47937;
padding: 0.25rem 0.4rem;
}
button.vitec-col-toggle:hover { color: #26358C; }
td.t3-page-column.vitec-col-collapsed > :not(:first-child) { display: none; }
@@ -0,0 +1,80 @@
/**
* Collapse toggle for .vitec-collapse previews in the page module.
*
* Two defenses against the page module's own interaction layer:
* - pointerdown in the CAPTURE phase toggles (fires before any click
* logic TYPO3 registered earlier),
* - the subsequent click on a summary is swallowed so it cannot trigger
* element editing.
* Requires pointer-events: auto on the collapse markup (see CSS) - the
* module disables pointer events on preview content by default.
*/
(function () {
function summaryOf(event) {
return event.target instanceof Element
? event.target.closest('details.vitec-collapse > summary')
: null;
}
document.addEventListener('pointerdown', function (event) {
const summary = summaryOf(event);
if (!summary) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
const details = summary.parentElement;
details.open = !details.open;
}, true);
document.addEventListener('click', function (event) {
if (summaryOf(event)) {
event.preventDefault();
event.stopImmediatePropagation();
}
}, true);
})();
/**
* Container-style collapse for whole page-module COLUMNS (the sub-market
* section slots and any other backend layout): a chevron button at the
* right of each column header folds the column down to its title row.
* State is remembered per page + colPos in localStorage.
*/
(function () {
function storageKey(colPos) {
const pageId = new URLSearchParams(window.location.search).get('id') || '0';
return 'vitecColCollapse_' + pageId + '_' + colPos;
}
function apply(column, collapsed, button) {
column.classList.toggle('vitec-col-collapsed', collapsed);
button.textContent = collapsed ? '▴' : '▾';
button.title = collapsed ? 'Section ausklappen' : 'Section einklappen';
}
function init() {
document.querySelectorAll('td.t3-page-column[data-colpos]').forEach(function (column) {
const header = column.querySelector('.t3-page-column-header');
if (!header || header.querySelector('.vitec-col-toggle')) {
return;
}
const colPos = column.dataset.colpos;
const button = document.createElement('button');
button.type = 'button';
button.className = 'vitec-col-toggle';
let collapsed = false;
try { collapsed = localStorage.getItem(storageKey(colPos)) === '1'; } catch (e) { /* egal */ }
apply(column, collapsed, button);
button.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
const nowCollapsed = !column.classList.contains('vitec-col-collapsed');
apply(column, nowCollapsed, button);
try { localStorage.setItem(storageKey(colPos), nowCollapsed ? '1' : '0'); } catch (e) { /* egal */ }
});
header.appendChild(button);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();