Add BG offset

This commit is contained in:
orasche committed 2026-08-14 10:59:22 +02:00
1 parent c720696781
commit 58990ebe8d
6 files changed
+161 -5

No files matched your search

@@ -81,6 +81,8 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
// 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_bg_pos_top', 'tx_vitec_bg_pos_bottom',
'tx_vitec_bg_pos_left', 'tx_vitec_bg_pos_right',
'tx_vitec_col1_align', 'tx_vitec_col1_justify',
'tx_vitec_col2_align', 'tx_vitec_col2_justify',
'tx_vitec_col3_align', 'tx_vitec_col3_justify',
@@ -74,7 +74,7 @@ class ContainerBackgroundRenderer
return (string)json_encode([
'image' => $image,
'size' => $this->resolveSize($row),
'position' => (string)($row['tx_vitec_bg_position'] ?? 'center center'),
'position' => $this->resolvePosition($row),
]);
} catch (\Throwable $e) {
return '';
@@ -101,4 +101,54 @@ class ContainerBackgroundRenderer
$percent = (int)($row['tx_vitec_bg_size_percent'] ?? 0);
return $percent > 0 ? $percent . '%' : 'auto';
}
/**
* The stored position, again resolved into one CSS-ready value.
*
* The four custom fields are edge offsets, but CSS cannot take a top AND a
* bottom offset at once, so they are folded into the percentage form. That
* form says exactly the same thing: in `background-position` a percentage
* aligns the same point of image and container, so "10% from the right edge"
* IS `90%`. Nothing is lost in the translation.
*
* Per axis the near edge wins when both are filled - `left` over `right`,
* `top` over `bottom` - because two opposing offsets contradict each other
* and silently picking one beats emitting something invalid.
*
* @param array<string,mixed> $row
*/
private function resolvePosition(array $row): string
{
$position = (string)($row['tx_vitec_bg_position'] ?? 'center center');
if ($position !== 'custom') {
return $position;
}
$x = $this->axisPercent($row, 'tx_vitec_bg_pos_left', 'tx_vitec_bg_pos_right');
$y = $this->axisPercent($row, 'tx_vitec_bg_pos_top', 'tx_vitec_bg_pos_bottom');
return $x . '% ' . $y . '%';
}
/**
* One axis as a percentage. An empty field means "centred on this axis" -
* which is why the columns are nullable: 0 is a legitimate offset (flush
* against that edge) and has to stay distinguishable from "not set".
*
* @param array<string,mixed> $row
*/
private function axisPercent(array $row, string $nearField, string $farField): int
{
$near = $row[$nearField] ?? null;
if ($near !== null && $near !== '') {
return (int)$near;
}
$far = $row[$farField] ?? null;
if ($far !== null && $far !== '') {
return 100 - (int)$far;
}
return 50;
}
}
@@ -11,6 +11,7 @@ use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Service\FlexFormService;
use Evomedien\Vitec\Service\LinkResolver;
use Evomedien\Vitec\Service\RteResolver;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
@@ -326,13 +327,14 @@ final class NewsJsonRenderer
{
$uid = (int)($news['uid'] ?? 0);
$pathSegment = (string)($news['path_segment'] ?? '');
$urls = $this->buildNewsUrls($pathSegment, $settings);
$urls = $this->buildNewsUrls($news, $settings);
return [
'uid' => $uid,
'title' => (string)($news['title'] ?? ''),
'alternativeTitle' => (string)($news['alternative_title'] ?? ''),
'pathSegment' => $pathSegment,
'type' => (string)($news['type'] ?? '0'),
'detailUrl' => $urls['detailUrl'],
'canonicalUrl' => $urls['canonicalUrl'],
'teaser' => (string)($news['teaser'] ?? ''),
@@ -352,12 +354,52 @@ final class NewsJsonRenderer
}
/**
* Where a news item actually points.
*
* EXT:news knows three record types, and only the first one lives on the
* news detail route:
*
* 0 article -> detail page + path_segment (the default below)
* 1 internal page -> the page picked in the backend; the record itself
* carries no body, so linking it to the detail route
* would show a teaser and hide the actual content
* 2 external url -> the given address, verbatim
*
* `detailUrl` is rewritten rather than accompanied by a second field, so a
* consumer can keep following one key for every item and never has to branch
* on the type. `type` is emitted alongside anyway — a front end may well want
* to open an external target in a new tab.
*
* Falls back to the article URL whenever a type 1/2 record does not yield a
* usable target: a link to the teaser beats a dead one.
*
* @param array<string,mixed> $news
* @param array<string,mixed> $settings
* @return array{detailUrl:string, canonicalUrl:string}
*/
private function buildNewsUrls(string $pathSegment, array $settings): array
private function buildNewsUrls(array $news, array $settings): array
{
$pathSegment = trim($pathSegment, '/');
$type = (string)($news['type'] ?? '0');
if ($type === '2') {
$external = trim((string)($news['externalurl'] ?? ''));
if ($external !== '') {
return ['detailUrl' => $external, 'canonicalUrl' => $external];
}
}
if ($type === '1') {
$pageUid = $this->extractPageUid((string)($news['internalurl'] ?? ''));
$pageUrl = $pageUid > 0 ? LinkResolver::pageUrl($pageUid) : null;
if ($pageUrl !== null && $pageUrl !== '') {
return [
'detailUrl' => $pageUrl,
'canonicalUrl' => $this->absoluteUrl($pageUrl),
];
}
}
$pathSegment = trim((string)($news['path_segment'] ?? ''), '/');
if ($pathSegment === '') {
return ['detailUrl' => '', 'canonicalUrl' => ''];
}
@@ -380,6 +422,19 @@ final class NewsJsonRenderer
];
}
/**
* `internalurl` holds either `t3://page?uid=N` or a bare uid, depending on
* how old the record is - the legacy import produced both forms.
*/
private function extractPageUid(string $internalUrl): int
{
$internalUrl = trim($internalUrl);
if (preg_match('#t3://page\?uid=(\d+)#i', $internalUrl, $m)) {
return (int)$m[1];
}
return ctype_digit($internalUrl) ? (int)$internalUrl : 0;
}
private function resolvePageSlug(int $pageId): string
{
if ($pageId <= 0) {