Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] Add custom page layout selector #2178

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading