From c720696781a2a54ba57dbca3c6d14e832c36ee42 Mon Sep 17 00:00:00 2001
From: Oliver Rasche
Date: Fri, 14 Aug 2026 10:36:36 +0200
Subject: [PATCH] Add Container Options
---
migrations/check_bg.php | 81 ++++++++++++++
.../ContainerChildrenProcessor.php | 5 +
.../UserFunc/ContainerBackgroundRenderer.php | 104 ++++++++++++++++++
.../UserFunc/CustomerlogosJsonRenderer.php | 18 ++-
.../UserFunc/EventlistJsonRenderer.php | 18 ++-
.../Classes/UserFunc/FormsJsonRenderer.php | 18 ++-
.../UserFunc/LocationsJsonRenderer.php | 17 ++-
.../UserFunc/MarketListJsonRenderer.php | 18 ++-
.../UserFunc/MarketShowJsonRenderer.php | 18 ++-
.../UserFunc/ModelcardJsonRenderer.php | 18 ++-
.../Classes/UserFunc/NewsJsonRenderer.php | 18 ++-
.../UserFunc/ProductListJsonRenderer.php | 18 ++-
.../UserFunc/ProductShowJsonRenderer.php | 18 ++-
.../UserFunc/SolutionShowJsonRenderer.php | 18 ++-
.../UserFunc/UsecaseListJsonRenderer.php | 18 ++-
.../UserFunc/UsecaseShowJsonRenderer.php | 18 ++-
.../Overrides/tt_content_vitec_container.php | 1 +
.../TCA/Overrides/tt_content_vitec_shared.php | 88 +++++++++++++++
.../Headless/vitec_containers.typoscript | 6 +
.../Private/Language/locallang_containers.xlf | 66 +++++++++++
packages/vitec/ext_tables.sql | 4 +
public/.htaccess | 14 +--
public/_frontend/index.html | 4 +-
23 files changed, 584 insertions(+), 22 deletions(-)
create mode 100644 migrations/check_bg.php
create mode 100644 packages/vitec/Classes/UserFunc/ContainerBackgroundRenderer.php
diff --git a/migrations/check_bg.php b/migrations/check_bg.php
new file mode 100644
index 0000000..2faede9
--- /dev/null
+++ b/migrations/check_bg.php
@@ -0,0 +1,81 @@
+ PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
+);
+
+echo "=== 1) Do the three columns exist? ===\n";
+$cols = $pdo->query("SHOW COLUMNS FROM tt_content LIKE 'tx_vitec_bg_%'")->fetchAll();
+if ($cols === []) {
+ echo " NONE. database:updateschema has not created them.\n";
+} else {
+ foreach ($cols as $c) {
+ echo sprintf(" %-24s %-18s default=%s\n", $c['Field'], $c['Type'], var_export($c['Default'], true));
+ }
+}
+
+echo "\n=== 2) The record itself (uid $uid) ===\n";
+$row = $pdo->query("SELECT uid, pid, CType, deleted, hidden, sys_language_uid, l18n_parent, tx_vitec_bg_variant, tx_vitec_bg_image, tx_vitec_bg_size, tx_vitec_bg_position FROM tt_content WHERE uid = $uid")->fetch();
+if (!$row) {
+ echo " no tt_content row with uid $uid\n";
+} else {
+ foreach ($row as $k => $v) {
+ echo sprintf(" %-24s %s\n", $k, var_export($v, true));
+ }
+}
+
+echo "\n=== 3) File references pointing at this record ===\n";
+$refs = $pdo->query(
+ "SELECT r.uid, r.tablenames, r.fieldname, r.uid_local, r.uid_foreign, r.deleted, r.hidden, r.sorting_foreign,
+ f.identifier, f.name, f.mime_type
+ FROM sys_file_reference r LEFT JOIN sys_file f ON f.uid = r.uid_local
+ WHERE r.uid_foreign = $uid AND r.tablenames = 'tt_content'"
+)->fetchAll();
+if ($refs === []) {
+ echo " NONE - nothing references this record. The image was not saved.\n";
+} else {
+ foreach ($refs as $r) {
+ echo sprintf(" ref %-6s field=%-22s file=%s deleted=%s hidden=%s\n",
+ $r['uid'], $r['fieldname'], (string)$r['identifier'], $r['deleted'], $r['hidden']);
+ }
+}
+
+echo "\n=== 4) Which fieldnames does tt_content use at all? (top 15) ===\n";
+foreach ($pdo->query("SELECT fieldname, COUNT(*) c FROM sys_file_reference WHERE tablenames='tt_content' AND deleted=0 GROUP BY fieldname ORDER BY c DESC LIMIT 15")->fetchAll() as $r) {
+ echo sprintf(" %-28s %d\n", $r['fieldname'], $r['c']);
+}
diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php
index c3c0ca6..8faba98 100755
--- a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php
+++ b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php
@@ -76,6 +76,11 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
*/
private const CONTAINER_FIELDS = [
'tx_vitec_gap',
+ // Background settings belong to the container, not to what sits inside
+ // it. They carry non-empty defaults ('none', 'cover', 'center center'),
+ // so without this they would show up in every single child's `data`.
+ 'tx_vitec_bg_variant', 'tx_vitec_bg_image', 'tx_vitec_bg_size',
+ 'tx_vitec_bg_size_percent', 'tx_vitec_bg_position',
'tx_vitec_col1_align', 'tx_vitec_col1_justify',
'tx_vitec_col2_align', 'tx_vitec_col2_justify',
'tx_vitec_col3_align', 'tx_vitec_col3_justify',
diff --git a/packages/vitec/Classes/UserFunc/ContainerBackgroundRenderer.php b/packages/vitec/Classes/UserFunc/ContainerBackgroundRenderer.php
new file mode 100644
index 0000000..6fcac0b
--- /dev/null
+++ b/packages/vitec/Classes/UserFunc/ContainerBackgroundRenderer.php
@@ -0,0 +1,104 @@
+cObj` dynamically. The renderer is
+ * handed over through this setter, and only when the method exists at all -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * `is_callable([$classObj, 'setContentObjectRenderer'])`. Without the method
+ * `$this->cObj` stays null and the userFunc never sees its own record.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
+ #[AsAllowedCallable]
+ public function render(string $content, array $conf): string
+ {
+ try {
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
+ if ($row === null) {
+ return '';
+ }
+
+ $uid = (int)($row['uid'] ?? 0);
+ if ($uid <= 0) {
+ return '';
+ }
+ // No shortcut on the tx_vitec_bg_image counter: sys_file_reference is
+ // the single source of truth, and a counter that is stale or missing
+ // (schema not updated yet) would silently swallow a set image. One
+ // indexed query per container is cheaper than that class of bug.
+
+ $image = GeneralUtility::makeInstance(UsecaseSerializer::class)
+ ->image($uid, 'tx_vitec_bg_image', 'tt_content');
+ if ($image === null) {
+ return '';
+ }
+
+ return (string)json_encode([
+ 'image' => $image,
+ 'size' => $this->resolveSize($row),
+ 'position' => (string)($row['tx_vitec_bg_position'] ?? 'center center'),
+ ]);
+ } catch (\Throwable $e) {
+ return '';
+ }
+ }
+
+ /**
+ * The stored size, resolved into something the front end can hand straight
+ * to CSS. `custom` is the only value that needs a second field; turning it
+ * into "80%" here keeps the promise that `size` is always usable as-is,
+ * instead of making every consumer look up a companion field.
+ *
+ * A custom size without a usable percentage falls back to `auto` - the CSS
+ * default - rather than emitting "0%" or the meaningless word "custom".
+ *
+ * @param array $row
+ */
+ private function resolveSize(array $row): string
+ {
+ $size = (string)($row['tx_vitec_bg_size'] ?? 'cover');
+ if ($size !== 'custom') {
+ return $size;
+ }
+ $percent = (int)($row['tx_vitec_bg_size_percent'] ?? 0);
+ return $percent > 0 ? $percent . '%' : 'auto';
+ }
+}
diff --git a/packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php b/packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php
index 94eb3c2..9a0f829 100755
--- a/packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/CustomerlogosJsonRenderer.php
@@ -11,6 +11,7 @@ use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render the VITEC customer logos as JSON (headless).
@@ -29,13 +30,28 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class CustomerlogosJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
private const TABLE = 'tx_vitec_domain_model_customer';
private const IMAGE_WIDTHS = [200, 400];
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_customerlogos') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php b/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php
index 80ced8f..4cdd77f 100755
--- a/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/EventlistJsonRenderer.php
@@ -13,6 +13,7 @@ use TYPO3\CMS\Core\Service\FlexFormService;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render the event list (tx_vitec_domain_model_event) as JSON.
@@ -30,6 +31,21 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class EventlistJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
/**
* Parent of the region categories. Hard-coded like the other taxonomy roots
* in this extension (Annex B-5 tracks them); making it a FlexForm setting
@@ -42,7 +58,7 @@ class EventlistJsonRenderer
public function render(string $content, array $conf): string
{
// 1) cObj data path
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_eventlist') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/FormsJsonRenderer.php b/packages/vitec/Classes/UserFunc/FormsJsonRenderer.php
index 9907def..8730cc1 100755
--- a/packages/vitec/Classes/UserFunc/FormsJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/FormsJsonRenderer.php
@@ -10,6 +10,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render ONE of the VITEC form plugins as JSON (headless).
@@ -25,10 +26,25 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class FormsJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && isset(FormDefinitions::CTYPE_MAP[(string)($row['CType'] ?? '')])) {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php b/packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php
index 065c7a4..0e8bad3 100755
--- a/packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/LocationsJsonRenderer.php
@@ -27,12 +27,27 @@ use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
*/
class LocationsJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
private const TABLE = 'tx_vitec_domain_model_location';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_locationlist') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/MarketListJsonRenderer.php b/packages/vitec/Classes/UserFunc/MarketListJsonRenderer.php
index 8af115b..2872087 100644
--- a/packages/vitec/Classes/UserFunc/MarketListJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/MarketListJsonRenderer.php
@@ -12,6 +12,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render all VITEC markets as JSON (headless).
@@ -38,13 +39,28 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class MarketListJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
private const TABLE = 'tx_vitec_domain_model_market';
private const CTYPE = 'vitec_marketlist';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === self::CTYPE) {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php
index 88ef70d..0020769 100755
--- a/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/MarketShowJsonRenderer.php
@@ -14,6 +14,7 @@ use Evomedien\Vitec\Service\LinkResolver;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render a single market as JSON for headless output.
@@ -24,11 +25,26 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class MarketShowJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_marketshow') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php b/packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php
index 365c1ad..c1239f8 100755
--- a/packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/ModelcardJsonRenderer.php
@@ -12,6 +12,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render the VITEC Card plugin as JSON (headless).
@@ -29,6 +30,21 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class ModelcardJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
private const MODEL_TABLES = [
'product' => 'tx_vitec_domain_model_product',
'story' => 'tx_vitec_domain_model_usecase',
@@ -39,7 +55,7 @@ class ModelcardJsonRenderer
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_modelcard') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php b/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php
index 568c1d7..f308e64 100644
--- a/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php
@@ -14,6 +14,7 @@ use TYPO3\CMS\Core\Service\FlexFormService;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Headless JSON renderer for EXT:news content elements.
@@ -25,6 +26,21 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
final class NewsJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
private const NEWS_CTYPES = [
'news_pi1',
'news_newsliststicky',
@@ -60,7 +76,7 @@ final class NewsJsonRenderer
public function render(string $content, array $conf): string
{
try {
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row !== null && in_array((string)($row['CType'] ?? ''), self::NEWS_CTYPES, true)) {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php
index 57de408..d4977a5 100755
--- a/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/ProductListJsonRenderer.php
@@ -19,6 +19,7 @@ use TYPO3\CMS\Core\Service\FlexFormService;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render product list as JSON for headless.
@@ -30,11 +31,26 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class ProductListJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_productlist') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php
index ab264ca..77f4330 100755
--- a/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/ProductShowJsonRenderer.php
@@ -17,6 +17,7 @@ use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Resource\FileReference;
use TYPO3\CMS\Extbase\Service\ImageService;
use Doctrine\DBAL\ParameterType;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render a single product as JSON for headless output.
@@ -28,11 +29,26 @@ use Doctrine\DBAL\ParameterType;
*/
class ProductShowJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_productshow') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php
index de83c5e..bb622dd 100755
--- a/packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/SolutionShowJsonRenderer.php
@@ -14,6 +14,7 @@ use Evomedien\Vitec\Service\LinkResolver;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc to render a single solution as JSON for headless output.
@@ -24,11 +25,26 @@ use TYPO3\CMS\Extbase\Service\ImageService;
*/
class SolutionShowJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
// 1) cObj data path
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_solutionshow') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php
index 47b9df2..7bf1705 100755
--- a/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/UsecaseListJsonRenderer.php
@@ -11,6 +11,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render the Success Story list as JSON (headless).
@@ -22,12 +23,27 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class UsecaseListJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
private const TABLE = 'tx_vitec_domain_model_usecase';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_usecaselist') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php
index f9bc676..40d322b 100755
--- a/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php
+++ b/packages/vitec/Classes/UserFunc/UsecaseShowJsonRenderer.php
@@ -11,6 +11,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Service\FlexFormService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* UserFunc: render a single Success Story as JSON (headless).
@@ -21,12 +22,27 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class UsecaseShowJsonRenderer
{
+ private ?ContentObjectRenderer $cObj = null;
+
+ /**
+ * TYPO3 v14 hands the ContentObjectRenderer over through this setter only -
+ * ContentObjectRenderer::callUserFunction() duck-types it with
+ * is_callable([$classObj, 'setContentObjectRenderer']). Without the method
+ * $this->cObj stays null, the cObj branch of render() never fires and the
+ * call falls through to page discovery, which picks the FIRST element of
+ * this CType on the page rather than the one actually being rendered.
+ */
+ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
+ {
+ $this->cObj = $cObj;
+ }
+
private const TABLE = 'tx_vitec_domain_model_usecase';
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
- $row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
+ $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null;
if ($row && (string)($row['CType'] ?? '') === 'vitec_usecaseshow') {
return $this->renderForRecord($row);
}
diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php
index 3a8088f..db81d74 100755
--- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php
+++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_container.php
@@ -34,6 +34,7 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
'--palette--;;general,
--palette--;;headers,
tx_vitec_bg_variant,
+ --palette--;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:bg_image.palette;vitec_background,
pi_flexform;LLL:EXT:vitec/Resources/Private/Language/locallang_containers.xlf:container.flexform.label,
--div--;LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:tabs.appearance,
--palette--;;frames,
diff --git a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php
index 654b6a6..c98dcb2 100755
--- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php
+++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php
@@ -27,6 +27,94 @@ use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
],
]);
+ // -------------------------------------------------------------------------
+ // Shared background image for the VITEC containers, with the two settings a
+ // background actually needs: how it scales and where it sits.
+ //
+ // Deliberately no crop variants (Clause 9.9): a background is framed by
+ // background-size / background-position in CSS, not by cropping the file.
+ // Cropping here would fight the two selects below.
+ //
+ // The values are written so the front end can hand them straight to CSS.
+ // `stretch` is the one exception - it has no CSS keyword and maps to
+ // `100% 100%`; spelled out here because "stretch" is what an editor looks
+ // for, not "100% 100%".
+ // -------------------------------------------------------------------------
+ ExtensionManagementUtility::addTCAcolumns('tt_content', [
+ 'tx_vitec_bg_image' => [
+ 'label' => $l . 'bg_image.label',
+ 'description' => $l . 'bg_image.description',
+ 'config' => [
+ 'type' => 'file',
+ 'maxitems' => 1,
+ 'allowed' => 'common-image-types',
+ ],
+ ],
+ 'tx_vitec_bg_size' => [
+ 'label' => $l . 'bg_size.label',
+ // Re-renders the form when the value changes, so the "Size %" field
+ // below appears the moment "Custom" is picked instead of only after
+ // a save. FormEngine keeps the current form state across the reload
+ // (SingleFieldContainer -> ReloadOnFieldChange); whether it asks
+ // first is the editor's own "verify on change" preference.
+ 'onChange' => 'reload',
+ 'config' => [
+ 'type' => 'select',
+ 'renderType' => 'selectSingle',
+ 'items' => [
+ ['label' => $l . 'bg_size.option.cover', 'value' => 'cover'],
+ ['label' => $l . 'bg_size.option.contain', 'value' => 'contain'],
+ ['label' => $l . 'bg_size.option.stretch', 'value' => 'stretch'],
+ ['label' => $l . 'bg_size.option.auto', 'value' => 'auto'],
+ ['label' => $l . 'bg_size.option.custom', 'value' => 'custom'],
+ ],
+ 'default' => 'cover',
+ ],
+ ],
+ // Only meaningful together with bg_size = custom, and hidden otherwise
+ // so the form never shows a percentage that has no effect. The renderer
+ // resolves it into the emitted `size` (e.g. "80%"), so the front end
+ // keeps getting one CSS-ready value instead of a second special case.
+ 'tx_vitec_bg_size_percent' => [
+ 'label' => $l . 'bg_size_percent.label',
+ 'description' => $l . 'bg_size_percent.description',
+ 'displayCond' => 'FIELD:tx_vitec_bg_size:=:custom',
+ 'config' => [
+ 'type' => 'number',
+ 'size' => 6,
+ 'default' => 100,
+ 'range' => [
+ 'lower' => 1,
+ 'upper' => 500,
+ ],
+ ],
+ ],
+ 'tx_vitec_bg_position' => [
+ 'label' => $l . 'bg_position.label',
+ 'config' => [
+ 'type' => 'select',
+ 'renderType' => 'selectSingle',
+ 'items' => [
+ ['label' => $l . 'bg_position.option.top_left', 'value' => 'left top'],
+ ['label' => $l . 'bg_position.option.top_center', 'value' => 'center top'],
+ ['label' => $l . 'bg_position.option.top_right', 'value' => 'right top'],
+ ['label' => $l . 'bg_position.option.center_left', 'value' => 'left center'],
+ ['label' => $l . 'bg_position.option.centered', 'value' => 'center center'],
+ ['label' => $l . 'bg_position.option.center_right', 'value' => 'right center'],
+ ['label' => $l . 'bg_position.option.bottom_left', 'value' => 'left bottom'],
+ ['label' => $l . 'bg_position.option.bottom_center', 'value' => 'center bottom'],
+ ['label' => $l . 'bg_position.option.bottom_right', 'value' => 'right bottom'],
+ ],
+ 'default' => 'center center',
+ ],
+ ],
+ ]);
+
+ // One palette, so size and position sit on a single row under the image.
+ $GLOBALS['TCA']['tt_content']['palettes']['vitec_background'] = [
+ 'showitem' => 'tx_vitec_bg_image, --linebreak--, tx_vitec_bg_size, tx_vitec_bg_size_percent, tx_vitec_bg_position',
+ ];
+
// -------------------------------------------------------------------------
// Shared parent-level setting: column gap scale (1–5) for the whole grid.
// Concerns the container itself (not a single column); maps to a spacing
diff --git a/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript b/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript
index 395e411..e70094a 100755
--- a/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript
+++ b/packages/vitec/Configuration/TypoScript/Headless/vitec_containers.typoscript
@@ -125,6 +125,12 @@ tt_content.vitec_container.fields.cssClass.data = flexform:pi_flexform:settings.
tt_content.vitec_container.fields.cssClass.stdWrap.ifEmpty.cObject = TEXT
tt_content.vitec_container.fields.cssClass.stdWrap.ifEmpty.cObject.value =
+# Optional background image with its two CSS settings. The renderer returns an
+# empty string when no image is set, so the `background` key stays out of the
+# payload entirely rather than appearing with a null image.
+tt_content.vitec_container.fields.background = USER
+tt_content.vitec_container.fields.background.userFunc = Evomedien\Vitec\UserFunc\ContainerBackgroundRenderer->render
+
# -----------------------------------------------------------------------------
# VITEC · Cards Carousel — container whose children (typically vitec_card
# elements) become the slides. Inherits the grid-container JSON structure
diff --git a/packages/vitec/Resources/Private/Language/locallang_containers.xlf b/packages/vitec/Resources/Private/Language/locallang_containers.xlf
index 2ae841d..1b398d4 100755
--- a/packages/vitec/Resources/Private/Language/locallang_containers.xlf
+++ b/packages/vitec/Resources/Private/Language/locallang_containers.xlf
@@ -178,6 +178,72 @@
5
+
+ Background Image
+
+
+ Background Image
+
+
+ Optional image behind the whole container. Leave empty for no background image.
+
+
+
+ Background Size
+
+
+ Cover (fill, may crop)
+
+
+ Contain (fit, may letterbox)
+
+
+ Stretch (distorts the image)
+
+
+ Auto (original size)
+
+
+ Custom (use Size % below)
+
+
+ Size %
+
+
+ Only used when Background Size is set to Custom. 100 = original width, 50 = half, 200 = double.
+
+
+
+ Background Position
+
+
+ Top Left
+
+
+ Top Center
+
+
+ Top Right
+
+
+ Center Left
+
+
+ Centered
+
+
+ Center Right
+
+
+ Bottom Left
+
+
+ Bottom Center
+
+
+ Bottom Right
+
+