From 78af83d2e316d4bd3d5467fba35cbff3bddcd8fe Mon Sep 17 00:00:00 2001 From: Oliver Rasche Date: Fri, 2 Oct 2026 15:00:05 +0200 Subject: [PATCH] - 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 --- .../Preview/ModelcardPreviewRenderer.php | 28 +++- .../Preview/SubmarketPluginPreview.php | 129 ++++++++++++++++++ .../Preview/UsecaseshowPluginPreview.php | 30 +++- .../TCA/Overrides/tt_content.php | 7 + .../card/templates/backend-preview.html | 11 ++ .../templates/backend-preview.html | 9 ++ .../templates/backend-preview.html | 29 +++- .../templates/backend-preview.html | 26 +++- .../templates/backend-preview.html | 26 +++- .../Templates/Preview/SubmarketSection.html | 30 ++++ .../Resources/Public/Css/backend-preview.css | 83 +++++++++++ .../JavaScript/backend-preview-collapse.js | 80 +++++++++++ 12 files changed, 465 insertions(+), 23 deletions(-) create mode 100644 packages/vitec/Classes/Preview/SubmarketPluginPreview.php create mode 100644 packages/vitec/Resources/Private/Templates/Preview/SubmarketSection.html create mode 100644 packages/vitec/Resources/Public/JavaScript/backend-preview-collapse.js diff --git a/packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php b/packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php index e628f44..f2647bf 100755 --- a/packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php +++ b/packages/vitec/Classes/Preview/ModelcardPreviewRenderer.php @@ -11,7 +11,6 @@ use TYPO3\CMS\Core\Database\ConnectionPool; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; /** * Page-module preview for the VITEC Card plugin (vitec_modelcard): @@ -45,7 +44,7 @@ class ModelcardPreviewRenderer extends StandardContentPreviewRenderer public function renderPageModulePreviewContent(GridColumnItem $item): string { try { - $record = $item->getRecord(); + $record = $this->recordArray($item); $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); $flexFormData = $flexFormService->convertFlexFormContentToArray($record['pi_flexform'] ?? ''); @@ -77,8 +76,12 @@ class ModelcardPreviewRenderer extends StandardContentPreviewRenderer } } - $view = GeneralUtility::makeInstance(StandaloneView::class); - $view->setTemplatePathAndFilename('EXT:vitec/Resources/Private/Templates/Preview/Modelcard.html'); + // 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/Modelcard.html', + request: $GLOBALS['TYPO3_REQUEST'] ?? null, + )); $view->assignMultiple([ 'typeLabel' => self::TYPE_LABELS[$modelType] ?? $modelType, 'layout' => $layout, @@ -124,4 +127,21 @@ class ModelcardPreviewRenderer extends StandardContentPreviewRenderer } 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 + */ + 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; + } } diff --git a/packages/vitec/Classes/Preview/SubmarketPluginPreview.php b/packages/vitec/Classes/Preview/SubmarketPluginPreview.php new file mode 100644 index 0000000..e491d13 --- /dev/null +++ b/packages/vitec/Classes/Preview/SubmarketPluginPreview.php @@ -0,0 +1,129 @@ + 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> + */ + 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 + */ + 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; + } +} diff --git a/packages/vitec/Classes/Preview/UsecaseshowPluginPreview.php b/packages/vitec/Classes/Preview/UsecaseshowPluginPreview.php index 59daefb..ed9098c 100644 --- a/packages/vitec/Classes/Preview/UsecaseshowPluginPreview.php +++ b/packages/vitec/Classes/Preview/UsecaseshowPluginPreview.php @@ -8,7 +8,6 @@ use TYPO3\CMS\Backend\Preview\StandardContentPreviewRenderer; use TYPO3\CMS\Backend\View\BackendLayout\Grid\GridColumnItem; use TYPO3\CMS\Core\Service\FlexFormService; use TYPO3\CMS\Core\Utility\GeneralUtility; -use TYPO3\CMS\Fluid\View\StandaloneView; use Evomedien\Vitec\Domain\Repository\UsecaseRepository; /** @@ -22,10 +21,14 @@ class UsecaseshowPluginPreview extends StandardContentPreviewRenderer */ public function renderPageModulePreviewContent(GridColumnItem $item): string { - $record = $item->getRecord(); + $record = $this->recordArray($item); - $view = GeneralUtility::makeInstance(StandaloneView::class); - $view->setTemplatePathAndFilename('EXT:vitec/Resources/Private/Templates/Preview/Usecaseshow.html'); + // 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/Usecaseshow.html', + request: $GLOBALS['TYPO3_REQUEST'] ?? null, + )); // Parse FlexForm data $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); @@ -69,7 +72,7 @@ class UsecaseshowPluginPreview extends StandardContentPreviewRenderer */ public function renderPageModulePreviewHeader(GridColumnItem $item): string { - $record = $item->getRecord(); + $record = $this->recordArray($item); // Parse FlexForm to get usecase info for header $flexFormService = GeneralUtility::makeInstance(FlexFormService::class); @@ -83,4 +86,21 @@ class UsecaseshowPluginPreview extends StandardContentPreviewRenderer return '🎯 Usecase Show Plugin (No usecase selected)'; } + + /** + * 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 + */ + 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; + } } \ No newline at end of file diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content.php b/packages/vitec/Configuration/TCA/Overrides/tt_content.php index f121e24..cd7ec39 100644 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content.php @@ -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.', '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'] = \Evomedien\Vitec\Preview\ModelcardPreviewRenderer::class; diff --git a/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html index 55636c9..5e16fb8 100755 --- a/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/card/templates/backend-preview.html @@ -3,7 +3,16 @@ + +
+ + + Card + {data.header} + Flip + +
Thumbnail: image > icon > placeholder @@ -151,6 +160,8 @@
+ +
diff --git a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/backend-preview.html index 096c79e..1585afd 100755 --- a/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/intro-paragraph/templates/backend-preview.html @@ -3,7 +3,14 @@ + +
+ + Intro Paragraph + {data.header} + +
VITEC · Intro Paragraph
@@ -83,6 +90,8 @@
+
+
diff --git a/packages/vitec/ContentBlocks/ContentElements/market-hero/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/market-hero/templates/backend-preview.html index 2475685..9b44e4e 100644 --- a/packages/vitec/ContentBlocks/ContentElements/market-hero/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/market-hero/templates/backend-preview.html @@ -1,12 +1,31 @@ + -
-
- Market Hero — {data.vitec_subline} - · 🎬 {data.vitec_video_title} + + +
+ + + + + 1 · Market Hero + {data.subline} + 🎬 Video + +
+

Subline: {data.subline}

+

CTA: {data.cta_label}Get in touch (Default)

+ +

Video: {data.video_title} — {data.video_url}

+
+ + +

Kein Hintergrundbild — Fallback: dunkler Verlauf.

+
-
+ + diff --git a/packages/vitec/ContentBlocks/ContentElements/market-insights/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/market-insights/templates/backend-preview.html index ac6a499..7195165 100644 --- a/packages/vitec/ContentBlocks/ContentElements/market-insights/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/market-insights/templates/backend-preview.html @@ -1,12 +1,28 @@ + -
-
- Market Insights — {data.vitec_text1} - · 🎬 {data.vitec_video_person} + + +
+ + + + + 6 · Industry Insights + {data.text1} + 🎬 {data.video_person} + +
+

{data.text1}

+

{data.text2}

+

CTA: {data.cta_label} → {data.cta_target}/success-storieskein Button

+ +

Video: {data.video_title} — {data.video_person}, {data.video_role}

+
-
+ + diff --git a/packages/vitec/ContentBlocks/ContentElements/market-intro/templates/backend-preview.html b/packages/vitec/ContentBlocks/ContentElements/market-intro/templates/backend-preview.html index 0087d15..4140819 100644 --- a/packages/vitec/ContentBlocks/ContentElements/market-intro/templates/backend-preview.html +++ b/packages/vitec/ContentBlocks/ContentElements/market-intro/templates/backend-preview.html @@ -1,11 +1,29 @@ + -
-
- Market Intro — {data.vitec_intro_text} + + +
+ + + + + 2 · Market Intro + {data.intro_text} + +
+

{data.intro_text}

+

+ Learn more: {data.learnmore_label} → {data.learnmore_target}kein Button + · Product Portfolio: sichtbarausgeblendet +

+ + +
-
+ + diff --git a/packages/vitec/Resources/Private/Templates/Preview/SubmarketSection.html b/packages/vitec/Resources/Private/Templates/Preview/SubmarketSection.html new file mode 100644 index 0000000..25eda87 --- /dev/null +++ b/packages/vitec/Resources/Private/Templates/Preview/SubmarketSection.html @@ -0,0 +1,30 @@ + + + + + +
+ + {label} + {hint} + +
+ +

Überschrift: {header}

+
+ + +
    + +
  1. {entry}
  2. +
    +
+
+ +

Keine Einzelauswahl.

+
+
+
+
+ + diff --git a/packages/vitec/Resources/Public/Css/backend-preview.css b/packages/vitec/Resources/Public/Css/backend-preview.css index f501d6d..48aa0c2 100644 --- a/packages/vitec/Resources/Public/Css/backend-preview.css +++ b/packages/vitec/Resources/Public/Css/backend-preview.css @@ -245,3 +245,86 @@ display: none; } } + +/* ============================================================= + Collapsible section previews (sub-market template, 2026-10-02) + Same practical effect as the container collapse on Two Columns, + built with
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; } diff --git a/packages/vitec/Resources/Public/JavaScript/backend-preview-collapse.js b/packages/vitec/Resources/Public/JavaScript/backend-preview-collapse.js new file mode 100644 index 0000000..516f485 --- /dev/null +++ b/packages/vitec/Resources/Public/JavaScript/backend-preview-collapse.js @@ -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(); + } +})();