diff --git a/Classes/Enum/ExtensionOption.php b/Classes/Enum/ExtensionOption.php
index f1164fef8..b2470dfa5 100644
--- a/Classes/Enum/ExtensionOption.php
+++ b/Classes/Enum/ExtensionOption.php
@@ -14,4 +14,5 @@ class ExtensionOption
public const OPTION_FLEXFORM_TO_IRRE = 'flexFormToIrre';
public const OPTION_INHERITANCE_MODE = 'inheritanceMode';
public const OPTION_UNIQUE_FILE_FIELD_NAMES = 'uniqueFileFieldNames';
+ public const OPTION_CUSTOM_PAGE_LAYOUT_SELECTOR = 'customLayoutSelector';
}
diff --git a/Classes/Integration/FormEngine/PageLayoutSelector.php b/Classes/Integration/FormEngine/PageLayoutSelector.php
new file mode 100644
index 000000000..78b6d26dc
--- /dev/null
+++ b/Classes/Integration/FormEngine/PageLayoutSelector.php
@@ -0,0 +1,155 @@
+nodeFactory = $nodeFactory ?? GeneralUtility::makeInstance(NodeFactory::class);
+ $this->data = $data;
+ $this->pageService = GeneralUtility::makeInstance(PageService::class);
+ $this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
+ $this->packageManager = GeneralUtility::makeInstance(PackageManager::class);
+ }
+
+ public function render(): array
+ {
+ $this->attachAssets();
+
+ $result = $this->initializeResultArray();
+
+ $selectedValue = $this->data['databaseRow'][$this->data['fieldName']] ?? null;
+
+ $fieldName = 'data' . $this->data['elementBaseName'];
+ $height = $this->data['parameterArray']['fieldConf']['config']['iconHeight'] ?? self::DEFAULT_ICON_WIDTH;
+ $renderTitle = $this->data['parameterArray']['fieldConf']['config']['titles'] ?? false;
+ $renderDescription = $this->data['parameterArray']['fieldConf']['config']['descriptions'] ?? false;
+
+ $templates = $this->pageService->getAvailablePageTemplateFiles();
+
+ $html = [];
+
+ $html[] = '
';
+
+ $result['html'] = implode(PHP_EOL, $html);
+
+ return $result;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ */
+ protected function resolveIconForForm(Form $form): string
+ {
+ $defaultIcon = 'EXT:flux/Resources/Public/Icons/Layout.svg';
+ $icon = MiscellaneousUtility::getIconForTemplate($form) ?? $defaultIcon;
+
+ if (!file_exists(GeneralUtility::getFileAbsFileName($icon))) {
+ $icon = PathUtility::getPublicResourceWebPath($defaultIcon);
+ } elseif (strpos($icon, 'EXT:') === 0) {
+ $icon = PathUtility::getPublicResourceWebPath($icon);
+ } elseif (($icon[0] ?? null) !== '/') {
+ $icon = PathUtility::getAbsoluteWebPath($icon);
+ }
+ return $icon;
+ }
+
+ /**
+ * @codeCoverageIgnore
+ */
+ protected function translate(string $label, string $extensionName): ?string
+ {
+ return LocalizationUtility::translate($label, $extensionName);
+ }
+
+ /**
+ * @codeCoverageIgnore
+ */
+ protected function attachAssets(): void
+ {
+ if (!self::$assetsIncluded) {
+ $this->pageRenderer->addCssFile('EXT:flux/Resources/Public/css/flux.css');
+
+ self::$assetsIncluded = true;
+ }
+ }
+}
diff --git a/Classes/Utility/ExtensionConfigurationUtility.php b/Classes/Utility/ExtensionConfigurationUtility.php
index 2077a8d05..2a4f745af 100644
--- a/Classes/Utility/ExtensionConfigurationUtility.php
+++ b/Classes/Utility/ExtensionConfigurationUtility.php
@@ -22,6 +22,7 @@ class ExtensionConfigurationUtility
ExtensionOption::OPTION_FLEXFORM_TO_IRRE => false,
ExtensionOption::OPTION_INHERITANCE_MODE => 'restricted',
ExtensionOption::OPTION_UNIQUE_FILE_FIELD_NAMES => false,
+ ExtensionOption::OPTION_CUSTOM_PAGE_LAYOUT_SELECTOR => false,
];
public static function initialize(?string $extensionConfiguration): void
diff --git a/Configuration/TCA/Overrides/pages.php b/Configuration/TCA/Overrides/pages.php
index ddb2a1905..be7746317 100644
--- a/Configuration/TCA/Overrides/pages.php
+++ b/Configuration/TCA/Overrides/pages.php
@@ -4,41 +4,42 @@
return;
}
+ if (version_compare(\TYPO3\CMS\Core\Utility\VersionNumberUtility::getCurrentTypo3Version(), '11.0', '>=') && \FluidTYPO3\Flux\Utility\ExtensionConfigurationUtility::getOption(\FluidTYPO3\Flux\Enum\ExtensionOption::OPTION_CUSTOM_PAGE_LAYOUT_SELECTOR)) {
+ $layoutSelectorFieldConfiguration = [
+ 'type' => 'radio',
+ 'items' => [],
+ 'renderType' => 'fluxPageLayoutSelector',
+ 'iconHeight' => 200,
+ 'titles' => true,
+ 'descriptions' => true,
+ ];
+ } else {
+ $layoutSelectorFieldConfiguration = [
+ 'type' => 'select',
+ 'renderType' => 'selectSingle',
+ 'behaviour' => [
+ 'allowLanguageSynchronization' => true,
+ ],
+ 'fieldWizard' => [
+ 'selectIcons' => [
+ 'disabled' => false,
+ ],
+ ],
+ ];
+ }
+
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('pages', [
'tx_fed_page_controller_action' => [
'exclude' => 1,
'label' => 'LLL:EXT:flux/Resources/Private/Language/locallang.xlf:pages.tx_fed_page_controller_action',
'onChange' => 'reload',
- 'config' => [
- 'type' => 'select',
- 'renderType' => 'selectSingle',
- 'behaviour' => [
- 'allowLanguageSynchronization' => true,
- ],
- 'fieldWizard' => [
- 'selectIcons' => [
- 'disabled' => false
- ]
- ]
- ]
+ 'config' => $layoutSelectorFieldConfiguration,
],
'tx_fed_page_controller_action_sub' => [
'exclude' => 1,
'label' => 'LLL:EXT:flux/Resources/Private/Language/locallang.xlf:pages.tx_fed_page_controller_action_sub',
'onChange' => 'reload',
-
- 'config' => [
- 'type' => 'select',
- 'renderType' => 'selectSingle',
- 'behaviour' => [
- 'allowLanguageSynchronization' => true,
- ],
- 'fieldWizard' => [
- 'selectIcons' => [
- 'disabled' => false
- ]
- ]
- ]
+ 'config' => $layoutSelectorFieldConfiguration,
],
'tx_fed_page_flexform' => [
'exclude' => 1,
diff --git a/Resources/Private/Language/locallang.xlf b/Resources/Private/Language/locallang.xlf
index d5c9611ae..c1652d625 100644
--- a/Resources/Private/Language/locallang.xlf
+++ b/Resources/Private/Language/locallang.xlf
@@ -42,6 +42,9 @@
+
+
+
diff --git a/Resources/Public/Icons/Layout.svg b/Resources/Public/Icons/Layout.svg
new file mode 100644
index 000000000..db911b093
--- /dev/null
+++ b/Resources/Public/Icons/Layout.svg
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
diff --git a/Resources/Public/css/flux.css b/Resources/Public/css/flux.css
index e02a449da..eb1d5350a 100644
--- a/Resources/Public/css/flux.css
+++ b/Resources/Public/css/flux.css
@@ -40,3 +40,39 @@
.flux-grid-hidden {
display: none !important;
}
+
+.flux-page-layouts {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ align-content: flex-start;
+}
+
+.flux-page-layouts input {
+ display: none;
+}
+
+.flux-page-layouts img {
+ display: block;
+}
+
+.flux-page-layouts label {
+ width: min-content;
+ display: inline-table;
+ flex-shrink: 1;
+ margin-right: 0.5em;
+ margin-bottom: 0.25em;
+ border: 3px solid lightgrey;
+ padding: 0.5em;
+}
+
+.flux-page-layouts label:hover,
+.flux-page-layouts label.selected {
+ border-color: black;
+}
+
+.flux-page-layouts label h4,
+.flux-page-layouts label p {
+ margin: 0;
+ margin-top: 0.5em;
+}
diff --git a/Tests/Unit/Integration/FormEngine/PageLayoutSelectorTest.php b/Tests/Unit/Integration/FormEngine/PageLayoutSelectorTest.php
new file mode 100644
index 000000000..78f281ec0
--- /dev/null
+++ b/Tests/Unit/Integration/FormEngine/PageLayoutSelectorTest.php
@@ -0,0 +1,128 @@
+markTestSkipped('Skipping test on TYPO3v10 - feature not supported on that version');
+ }
+ parent::setUp();
+
+ $this->singletons = GeneralUtility::getSingletonInstances();
+
+ $pageService = $this->getMockBuilder(PageService::class)->disableOriginalConstructor()->getMock();
+ $packageManager = $this->getMockBuilder(PackageManager::class)->disableOriginalConstructor()->getMock();
+ $pageRenderer = $this->getMockBuilder(PageRenderer::class)->disableOriginalConstructor()->getMock();
+
+ $packageMetaData = $this->getMockBuilder(MetaData::class)->disableOriginalConstructor()->getMock();
+ if (method_exists($packageMetaData, 'getTitle')) {
+ $packageMetaData->method('getTitle')->willReturn('package-title');
+ } else {
+ $packageMetaData->method('getPackageKey')->willReturn('foobar');
+ }
+
+ $package = $this->getMockBuilder(Package::class)->disableOriginalConstructor()->getMock();
+ $package->method('getPackageMetaData')->willReturn($packageMetaData);
+ $packageManager->method('getPackage')->willReturn($package);
+
+ $form = Form::create();
+ $form->setOption(FormOption::TEMPLATE_FILE_RELATIVE, 'Foobar');
+ $form->setDescription('description');
+ $form->setLabel('title');
+ $pageService->method('getAvailablePageTemplateFiles')->willReturn(['Foo.Bar' => [$form]]);
+
+ GeneralUtility::setSingletonInstance(PageService::class, $pageService);
+ GeneralUtility::setSingletonInstance(PackageManager::class, $packageManager);
+ GeneralUtility::setSingletonInstance(PageRenderer::class, $pageRenderer);
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ GeneralUtility::resetSingletonInstances($this->singletons);
+ }
+
+ public function testRenderWithSelectedValue(): void
+ {
+ $html = $this->executeTest('Foo.Bar->foobar');
+ self::assertStringContainsString(
+ '',
+ $html
+ );
+ self::assertStringContainsString('Parent decides', $html);
+ self::assertStringContainsString('Foobar
', $html);
+ self::assertStringContainsString('description
', $html);
+ }
+
+ public function testRenderWithoutSelectedValue(): void
+ {
+ $html = $this->executeTest('');
+ self::assertStringContainsString(
+ '',
+ $html
+ );
+ self::assertStringNotContainsString(
+ '',
+ $html
+ );
+ self::assertStringContainsString('Parent decides', $html);
+ self::assertStringContainsString('Foobar
', $html);
+ self::assertStringContainsString('description
', $html);
+ }
+
+ private function executeTest(string $value): string
+ {
+ $nodeFactory = $this->getMockBuilder(NodeFactory::class)->disableOriginalConstructor()->getMock();
+ $data = [
+ 'parameterArray' => ['foo' => 'bar'],
+ 'fieldName' => 'field',
+ 'elementBaseName' => 'elementBaseName',
+ 'parameterArray' => [
+ 'fieldConf' => [
+ 'config' => [
+ 'iconHeight' => 300,
+ 'titles' => true,
+ 'descriptions' => true,
+ ],
+ ],
+ ],
+ 'databaseRow' => [
+ 'field' => $value,
+ ],
+ ];
+ $subject = $this->getMockBuilder(PageLayoutSelector::class)
+ ->onlyMethods(['initializeResultArray', 'resolveIconForForm', 'translate', 'attachAssets'])
+ ->setConstructorArgs([$nodeFactory, $data])
+ ->getMock();
+ $subject->method('initializeResultArray')->willReturn([]);
+ $subject->method('resolveIconForForm')->willReturn('icon');
+ $subject->method('translate')->willReturnArgument('translated');
+
+ return $subject->render()['html'];
+ }
+}
diff --git a/ext_conf_template.txt b/ext_conf_template.txt
index 31116620a..a8973411a 100644
--- a/ext_conf_template.txt
+++ b/ext_conf_template.txt
@@ -27,3 +27,6 @@ inheritanceMode = restricted
# cat=basic/enable; type=boolean; label=LLL:EXT:flux/Resources/Private/Language/locallang.xlf:extension_configuration.uniqueFileFieldNames
uniqueFileFieldNames = 0
+
+# cat=basic/enable; type=boolean; label=LLL:EXT:flux/Resources/Private/Language/locallang.xlf:extension_configuration.customLayoutSelector
+customLayoutSelector = 0
diff --git a/ext_localconf.php b/ext_localconf.php
index e46fdcb76..9b3e7124a 100644
--- a/ext_localconf.php
+++ b/ext_localconf.php
@@ -66,6 +66,11 @@
];
// FormEngine integration for custom TCA field types used by Flux
+ $GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1726225012] = [
+ 'nodeName' => 'fluxPageLayoutSelector',
+ 'priority' => 40,
+ 'class' => \FluidTYPO3\Flux\Integration\FormEngine\PageLayoutSelector::class,
+ ];
$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1575276512] = [
'nodeName' => 'fluxContentTypeValidator',
'priority' => 40,
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon
index bb60f7805..4b6ccaeb6 100644
--- a/phpstan-baseline.neon
+++ b/phpstan-baseline.neon
@@ -1,5 +1,17 @@
parameters:
ignoreErrors:
+ -
+ message: "#^Call to an undefined method TYPO3\\\\CMS\\\\Core\\\\Package\\\\MetaData\\:\\:getTitle\\(\\)\\.$#"
+ count: 1
+ path: Classes/Integration/FormEngine/PageLayoutSelector.php
+ -
+ message: "#^Call to an undefined static method TYPO3\\\\CMS\\\\Core\\\\Utility\\\\PathUtility\\:\\:getPublicResourceWebPath\\(\\)\\.$#"
+ count: 2
+ path: Classes/Integration/FormEngine/PageLayoutSelector.php
+ -
+ message: "#^Property .+ does not accept#"
+ count: 4
+ path: Classes/Integration/FormEngine/PageLayoutSelector.php
-
message: "#^Variable \\$EM_CONF might not be defined\\.$#"
count: 1