Skip to content
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
14 changes: 0 additions & 14 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ trim_trailing_whitespace = true
[*.{ts,js}]
indent_size = 2

# JSON-Files
[*.json]
indent_style = tab

# ReST-Files
[*.rst]
indent_size = 4
Expand All @@ -32,7 +28,6 @@ indent_size = 2
# NEON-Files
[*.neon]
indent_size = 2
indent_style = tab

# package.json
[package.json]
Expand All @@ -42,15 +37,6 @@ indent_size = 2
[*.{typoscript,tsconfig}]
indent_size = 2

# XLF-Files
[*.xlf]
indent_style = tab

# SQL-Files
[*.sql]
indent_style = tab
indent_size = 2

# .htaccess
[{_.htaccess,.htaccess}]
indent_style = tab
5 changes: 5 additions & 0 deletions Classes/Configuration/Extension.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ public static function addTypoScriptSetup(): void
'));
}

/**
* @todo Remove hook when dropping v13 support.
* @see https://docs.typo3.org/c/typo3/cms-core/main/en-us//Changelog/14.0/Breaking-107566-RemovedAfterInitializeCurrentPageHook.html
* @see https://docs.typo3.org/c/typo3/cms-core/main/en-us//Changelog/14.0/Feature-107566-IntroducePSR14AfterCurrentPageIsResolvedEvent.html#feature-107566-1759226649
*/
public static function registerHooks(): void
{
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/form']['afterInitializeCurrentPage'][1571076908] = FormElementLinkResolverHook::class;
Expand Down
288 changes: 288 additions & 0 deletions Classes/EventListener/AfterCurrentPagIsResolvedEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 CMS extension "form_element_linked_checkbox".
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/

namespace TRITUM\FormElementLinkedCheckbox\EventListener;

use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Form\Domain\Model\FormElements\GenericFormElement;
use TYPO3\CMS\Form\Domain\Model\Renderable\RootRenderableInterface;
use TYPO3\CMS\Form\Domain\Runtime\FormRuntime;
use TYPO3\CMS\Form\Event\AfterCurrentPageIsResolvedEvent;
use TYPO3\CMS\Form\Service\TranslationService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;

class AfterCurrentPagIsResolvedEventListener
{
/**
* @var string Form element type to match
*/
private string $type = 'LinkedCheckbox';

/**
* @var FormRuntime The current form runtime
*/
private FormRuntime $formRuntime;

#[AsEventListener('form-element-linked-checkbox/after-current-page-is-resolved-event')]
public function __invoke(AfterCurrentPageIsResolvedEvent $event): void
{
$this->formRuntime = $event->formRuntime;
$renderables = $this->formRuntime->getFormDefinition()->getRenderablesRecursively();

foreach ($renderables as $renderable) {
$this->processCharacterSubstitution($renderable);
}
}

/**
* Resolve link in label of form elements with type LinkedCheckbox.
*
* @param RootRenderableInterface $renderable
*/
private function processCharacterSubstitution(RootRenderableInterface $renderable): void
{
// Only process linkText parsing if renderable matches given type
if (!($renderable instanceof GenericFormElement) || $renderable->getType() !== $this->type) {
return;
}

$label = $this->translate($renderable, ['label']);
$properties = $renderable->getProperties();

// Check if form element label contains any argument flags such as %s.
// This also checks if one tries to use the percent sign as regular
// character instead of a flag marked for inserting the translated
// linkText. It needs to be set as double-percent (%%) substring.
// If character substitution is NOT requested, enforce the link to
// be prepended to the label text.
if (!self::needsCharacterSubstitution($label)) {
$label .= ' %s';
}

// Resolve all label arguments and merge them together in order to
// use it for later translation of the label. The following
// configuration methods are considered:
// - "single configuration" via properties pageUid / linkText
// - "array configuration" via property "additionalLinks"
$singleLinkArgument = $this->buildArgumentFromSingleConfiguration($renderable);
$additionalLinkArguments = $this->buildArgumentsFromArrayConfiguration($renderable);
$labelArguments = array_merge([$singleLinkArgument], $additionalLinkArguments);

// Provide translated link as argument for the form element label
$renderable->setRenderingOption('translation', [
'arguments' => [
'label' => $labelArguments,
],
]);

// Run translation again and override final label
// (with translated links) as well as it will be used
// as default value if no translation is provided
$translatedLabel = vsprintf($label, $labelArguments);
if (is_string($translatedLabel)) {
$renderable->setLabel($translatedLabel);
}

// Reset custom properties in order to avoid additional
// link rendering in template
$renderable->setProperty('linkText', null);
$renderable->setProperty('pageUid', null);
$renderable->setProperty('additionalLinks', null);

// Set fallback value to original property values
// to allow other hooks making use of these ones
$renderable->setProperty('_label', $label);
$renderable->setProperty('_linkText', $singleLinkArgument);
$renderable->setProperty('_pageUid', (int)$properties['pageUid']);
$renderable->setProperty('_additionalLinks', $additionalLinkArguments);
$renderable->setProperty('_linksProcessed', true);
}

/**
* Build translation argument for label from single configuration.
*
* Returns the resolved argument from properties "pageUid" and "linkText"
* (default configuration).
*
* @param GenericFormElement $element
*
* @return string
*/
private function buildArgumentFromSingleConfiguration(GenericFormElement $element): string
{
$properties = $element->getProperties();
$pageUid = (int)$properties['pageUid'];

return $this->buildArgument($element, ['linkText'], $pageUid);
}

/**
* Build translation arguments for label from array configuration.
*
* Returns the resolved arguments from property "additionalLinks". The
* property consists of a key/value combination of "pageUid"/"linkText".
*
* @return string[]
*/
private function buildArgumentsFromArrayConfiguration(GenericFormElement $element): array
{
if (!$this->hasAdditionalLinksConfigured($element)) {
return [];
}

$properties = $element->getProperties();
$arguments = [];

foreach ($properties['additionalLinks'] as $pageUid => $linkText) {
$arguments[$pageUid] = $this->buildArgument($element, ['additionalLinks', $pageUid], (int)$pageUid);
}

return $arguments;
}

/**
* Build translation argument for label from given property path to link text.
*
* Returns the translation argument for the given property path. The property
* path describes the path to the link text for the current argument, whereas
* the pageUid describes the actual target page. If the pageUid is valid, this
* method returns the generated link, otherwise the translated link text.
*
* @param GenericFormElement $element
* @param string[] $linkTextPropertyPath
* @param int $pageUid
*
* @return string
*/
private function buildArgument(GenericFormElement $element, array $linkTextPropertyPath, int $pageUid): string
{
$translatedLinkText = $this->translate($element, $linkTextPropertyPath);
$additionalLinkConfiguration = $element->getRenderingOptions()['linkConfiguration'] ?? [];

if ($pageUid <= 0) {
return $translatedLinkText;
}

return $this->buildLinkFromPageUid($translatedLinkText, $pageUid, $additionalLinkConfiguration);
}

/**
* Check whether renderable has additional links configured.
*
* Returns `true` if the current renderable has at least one "additional link"
* configured (via property "additionalLinks").
*
* @param GenericFormElement $element
*
* @return bool
*/
private function hasAdditionalLinksConfigured(GenericFormElement $element): bool
{
$properties = $element->getProperties();

return is_array($properties['additionalLinks'] ?? null) && $properties['additionalLinks'] !== [];
}

/**
* Translate form element property by given path.
*
* @param RootRenderableInterface $renderable
* @param string[] $propertyPath
*
* @return string
*/
private function translate(RootRenderableInterface $renderable, array $propertyPath): string
{
$translationService = GeneralUtility::makeInstance(TranslationService::class);
$value = $translationService->translateFormElementValue($renderable, $propertyPath, $this->formRuntime);

if (!is_string($value)) {
return '';
}

return $value;
}

/**
* Build typolink from given page UID and additional configuration.
*
* @param string $linkText
* @param int $pageUid
* @param array<string, string|int> $additionalAttributes
*
* @return string
*/
private function buildLinkFromPageUid(string $linkText, int $pageUid, array $additionalAttributes = []): string
{
if (!$pageUid) {
return $linkText;
}

// Build typolink configuration from pageUid and additional attributes:
// As the pageUid is a necessary part of the parameter configuration,
// it cannot be overridden by $additionalAttributes. However one can
// provide additional parameter configuration by making use of the
// "parameter" key. This way one can disable the default link target
// behaviour which falls back to "_blank" by providing an empty
// value for the configuration key "parameter" or just setting any
// different parameter values according to the TypoScript reference.
$parameter = $pageUid . ' ';
if (array_key_exists('parameter', $additionalAttributes)) {
$parameter .= $additionalAttributes['parameter'];
} else {
$parameter .= '_blank';
}
$configuration = [
'typolink.' => [
'parameter' => trim($parameter),
'forceAbsoluteUrl' => true,
],
];
if ($additionalAttributes) {
unset($additionalAttributes['parameter']);
ArrayUtility::mergeRecursiveWithOverrule($configuration['typolink.'], $additionalAttributes);
}

$contentObject = GeneralUtility::makeInstance(ContentObjectRenderer::class);
$contentObject->start([], '');

return $contentObject->stdWrap($linkText, $configuration) ?: $linkText;
}

/**
* Check whether the given string needs character substitution.
*
* This method checks whether a given string contains substitution characters (%) which will be used
* for character substitution using the `printf()` function. Substitution characters can be escaped
* by an additional character (%%) and will be excluded from the check.
*
* @param string $value String to test for the need of character substitution
*
* @return bool `true` if character substitution is needed, `false` otherwise
* @see printf()
*/
private static function needsCharacterSubstitution(string $value): bool
{
$filteredValue = $value;
do {
$filteredValue = str_replace('%%', '', $filteredValue);
} while (str_contains($filteredValue, '%%'));
return str_contains($filteredValue, '%');
}
}
4 changes: 2 additions & 2 deletions Classes/Hooks/FormElementLinkResolverHook.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,12 @@ final class FormElementLinkResolverHook
/**
* @var string Form element type to match
*/
private $type = 'LinkedCheckbox';
private string $type = 'LinkedCheckbox';

/**
* @var FormRuntime The current form runtime
*/
private $formRuntime;
private FormRuntime $formRuntime;

/**
* Resolve link in label of form elements with type LinkedCheckbox.
Expand Down
8 changes: 8 additions & 0 deletions Configuration/Services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
_defaults:
autowire: true
autoconfigure: true
public: false

TRITUM\FormElementLinkedCheckbox\:
resource: '../Classes/*'
6 changes: 4 additions & 2 deletions Configuration/TCA/Overrides/sys_template.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?php

use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;

defined('TYPO3') or die();

call_user_func(function () {
\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile(
call_user_func(function (): void {
ExtensionManagementUtility::addStaticFile(
'form_element_linked_checkbox',
'Configuration/TypoScript',
'Linked checkbox configuration'
Expand Down
Loading