diff --git a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php index 8faba98..e22c7b4 100755 --- a/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php +++ b/packages/vitec/Classes/DataProcessing/ContainerChildrenProcessor.php @@ -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', diff --git a/packages/vitec/Classes/UserFunc/ContainerBackgroundRenderer.php b/packages/vitec/Classes/UserFunc/ContainerBackgroundRenderer.php index 6fcac0b..7a84f94 100644 --- a/packages/vitec/Classes/UserFunc/ContainerBackgroundRenderer.php +++ b/packages/vitec/Classes/UserFunc/ContainerBackgroundRenderer.php @@ -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 $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 $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; + } } diff --git a/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php b/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php index f308e64..9e2c932 100644 --- a/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php +++ b/packages/vitec/Classes/UserFunc/NewsJsonRenderer.php @@ -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 $news * @param array $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) { 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 c98dcb2..ba18d7b 100755 --- a/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php +++ b/packages/vitec/Configuration/TCA/Overrides/tt_content_vitec_shared.php @@ -91,6 +91,8 @@ use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; ], 'tx_vitec_bg_position' => [ 'label' => $l . 'bg_position.label', + // Same reason as on bg_size: reveal the custom fields immediately. + 'onChange' => 'reload', 'config' => [ 'type' => 'select', 'renderType' => 'selectSingle', @@ -104,15 +106,43 @@ use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; ['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'], + ['label' => $l . 'bg_position.option.custom', 'value' => 'custom'], ], 'default' => 'center center', ], ], ]); + // Custom position: four edge offsets in percent, shown only when the select + // is on "custom". `nullable` matters here - an empty field has to stay + // distinguishable from an explicit 0, which is a valid offset (flush against + // that edge). Empty means "centred on this axis". + // + // CSS cannot take a top AND a bottom offset at once, so these four are not + // written out literally; ContainerBackgroundRenderer folds them into the + // percentage form, where "10% from the right" is simply 90%. + $positionOffsets = []; + foreach (['top', 'bottom', 'left', 'right'] as $edge) { + $positionOffsets['tx_vitec_bg_pos_' . $edge] = [ + 'label' => $l . 'bg_pos.' . $edge, + 'displayCond' => 'FIELD:tx_vitec_bg_position:=:custom', + 'config' => [ + 'type' => 'number', + 'size' => 5, + 'nullable' => true, + 'range' => [ + 'lower' => 0, + 'upper' => 100, + ], + ], + ]; + } + ExtensionManagementUtility::addTCAcolumns('tt_content', $positionOffsets); + // 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', + 'showitem' => 'tx_vitec_bg_image, --linebreak--, tx_vitec_bg_size, tx_vitec_bg_size_percent, tx_vitec_bg_position,' + . ' --linebreak--, tx_vitec_bg_pos_top, tx_vitec_bg_pos_bottom, tx_vitec_bg_pos_left, tx_vitec_bg_pos_right', ]; // ------------------------------------------------------------------------- diff --git a/packages/vitec/Resources/Private/Language/locallang_containers.xlf b/packages/vitec/Resources/Private/Language/locallang_containers.xlf index 1b398d4..4c5ab67 100755 --- a/packages/vitec/Resources/Private/Language/locallang_containers.xlf +++ b/packages/vitec/Resources/Private/Language/locallang_containers.xlf @@ -243,6 +243,21 @@ Bottom Right + + Custom (use the offsets below) + + + Top % + + + Bottom % + + + Left % + + + Right % + diff --git a/packages/vitec/ext_tables.sql b/packages/vitec/ext_tables.sql index 4f59a59..10b876a 100755 --- a/packages/vitec/ext_tables.sql +++ b/packages/vitec/ext_tables.sql @@ -211,6 +211,10 @@ CREATE TABLE tt_content ( tx_vitec_bg_size VARCHAR(20) DEFAULT 'cover' NOT NULL, tx_vitec_bg_size_percent int(11) unsigned DEFAULT '100' NOT NULL, tx_vitec_bg_position VARCHAR(20) DEFAULT 'center center' NOT NULL, + tx_vitec_bg_pos_top int(11) DEFAULT NULL, + tx_vitec_bg_pos_bottom int(11) DEFAULT NULL, + tx_vitec_bg_pos_left int(11) DEFAULT NULL, + tx_vitec_bg_pos_right int(11) DEFAULT NULL, tx_vitec_usecase_content int(11) unsigned DEFAULT '0' NOT NULL );