Skip to content

Commit

Permalink
[FEATURE] Add custom page layout selector
Browse files Browse the repository at this point in the history
Note: Not available on TYPO3v10.

Adds a replacement page layout selector which can be
enabled instead of the default TCA "select" type.

The page layout selector is enabled in extension
configuration and can be configured through TCA
overrides (in Configuration/TCA/Overrides/pages.php
of an extension that depends on Flux):

```
// For both tx_fed_page_controller_action and tx_fed_page_controller_action_sub
// and can be configured individually, e.g. to make sub-layout icons smaller
// or only render titles/descriptions for "this page" layouts.

$GLOBALS['TCA']['pages']['columns']['tx_fed_page_controller_action']['config']['iconHeight'] = 200;
$GLOBALS['TCA']['pages']['columns']['tx_fed_page_controller_action']['config']['titles'] = true;
$GLOBALS['TCA']['pages']['columns']['tx_fed_page_controller_action']['config']['descriptions'] = true;
```
  • Loading branch information
NamelessCoder committed Sep 16, 2024
1 parent ce6713d commit 150b42c
Show file tree
Hide file tree
Showing 11 changed files with 375 additions and 25 deletions.
1 change: 1 addition & 0 deletions Classes/Enum/ExtensionOption.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
155 changes: 155 additions & 0 deletions Classes/Integration/FormEngine/PageLayoutSelector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
<?php
namespace FluidTYPO3\Flux\Integration\FormEngine;

/*
* This file is part of the FluidTYPO3/Flux project under GPLv2 or later.
*
* For the full copyright and license information, please read the
* LICENSE.md file that was distributed with this source code.
*/

use FluidTYPO3\Flux\Enum\FormOption;
use FluidTYPO3\Flux\Form;
use FluidTYPO3\Flux\Service\PageService;
use FluidTYPO3\Flux\Utility\ExtensionNamingUtility;
use FluidTYPO3\Flux\Utility\MiscellaneousUtility;
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;

class PageLayoutSelector extends AbstractNode
{
private const DEFAULT_ICON_WIDTH = 200;

private static bool $assetsIncluded = false;

private PageService $pageService;
private PageRenderer $pageRenderer;
private PackageManager $packageManager;

public function __construct(?NodeFactory $nodeFactory = null, array $data = [])
{
$this->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[] = '<div>';
$html[] = '<label>';
$html[] = '<input type="radio" name="' .
$fieldName .
'" value=""' .
(empty($selectedValue) ? ' checked="checked"' : '') .
' />';
$html[] = 'Parent decides';
$html[] = '</label>';

foreach ($templates as $groupName => $group) {
$extensionName = ExtensionNamingUtility::getExtensionName($groupName);
$packageInfo = $this->packageManager->getPackage(ExtensionNamingUtility::getExtensionKey($groupName));

$html[] = '<h2>' . ($packageInfo->getPackageMetaData()->getTitle() ?? $groupName) . '</h2>';
$html[] = '<fieldset class="flux-page-layouts">';
foreach ($group as $form) {
$icon = $this->resolveIconForForm($form);

/** @var string $templateName */
$templateName = $form->getOption(FormOption::TEMPLATE_FILE_RELATIVE);
$identifier = $groupName . '->' . lcfirst($templateName);

$html[] = '<label' . ($selectedValue === $identifier ? ' class="selected"' : '') . '>';
$html[] = '<input type="radio" name="' .
$fieldName .
'" value="' .
$identifier .
'"' .
($selectedValue === $identifier ? ' checked="checked"' : '') .
' />';
$html[] = '<img src="' . $icon .'" style="height: ' . $height . 'px" />';
if ($renderTitle && ($title = $form->getLabel())) {
$title = $this->translate((string) $title, $extensionName) ?? $templateName;
if (strpos($title, 'LLL:EXT:') !== 0) {
$html[] = '<h4>' . $title . '</h4>';
}
}

if ($renderDescription && ($description = $form->getDescription())) {
$description = $this->translate((string) $description, $extensionName) ?? $description;
if (strpos($description, 'LLL:EXT:') !== 0) {
$html[] = '<p>' . $description . '</p>';
}
}

$html[] = '</label>';
}
$html[] = '</fieldset>';
}

$html[] = '</div>';

$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;
}
}
}
1 change: 1 addition & 0 deletions Classes/Utility/ExtensionConfigurationUtility.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 26 additions & 25 deletions Configuration/TCA/Overrides/pages.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions Resources/Private/Language/locallang.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
<trans-unit id="extension_configuration.uniqueFileFieldNames">
<source>Unique file field names: When this is enabled, FAL reference fields within a Flux context will be prefixed with the parent field name. This is done in order to ensure that references written to sys_file_reference will contain a unique value in "fieldname". Without this option enabled, any record which renders the same Flux form in two fields (e.g. pages when "this page" and "subpages" templates are identical) will show the same references in both fields because the "fieldname" saved in sys_file_reference is the same for both contexts. So, in order to avoid this duplication symptom, you can prefix the "fieldname" value stored in DB. Note: although this is handled transparently when you use transform="file" (and other file transformations) the prefix must manually be added to the field name when using e.g. v:resources.record.fal. CHANGING THIS OPTION WILL ORPHAN ALL EXISTING RELATIONS - TOGGLE IT ON FOR NEW SITES BUT LEAVE OLD SITES USING THE OPTION VALUE THEY WERE BORN WITH, OR YOU WILL NEED TO MIGRATE YOUR FILE REFERENCES!</source>
</trans-unit>
<trans-unit id="extension_configuration.customLayoutSelector">
<source>Custom Layout Selector: (Requires TYPO3v11 or higher). When enabled, uses a fully custom page layout selector instead of the default "select" type for the "Page Layouts" fields in page properties. The custom page layout selector can be further configured through TCA overrides for TCA.pages.columns.tx_fed_page_controller_action.config and TCA.pages.columns.tx_fed_page_controller_action_sub.config. Supported settings are: "iconHeight" (integer, sets the height of icons, defaults to 200), titles (boolean, whether to render template titles in the selector, defaults to true) and descriptions (boolean, whether to render template descriptions in the selector, defaults to true).</source>
</trans-unit>
<trans-unit id="content_types">
<source>Flux-based Content Type</source>
</trans-unit>
Expand Down
5 changes: 5 additions & 0 deletions Resources/Public/Icons/Layout.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 changes: 36 additions & 0 deletions Resources/Public/css/flux.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading

0 comments on commit 150b42c

Please sign in to comment.