TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:00 +02:00
commit f9941541b7
1178 changed files with 135377 additions and 0 deletions
@@ -0,0 +1,451 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Form\AbstractNode;
use TYPO3\CMS\Backend\Form\Behavior\OnFieldChangeTrait;
use TYPO3\CMS\Backend\Form\Behavior\UpdateBitmaskOnFieldChange;
use TYPO3\CMS\Backend\Form\NodeFactory;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Domain\DateTimeFactory;
use TYPO3\CMS\Core\Localization\DateFormatter;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Localization\Locale;
use TYPO3\CMS\Core\Localization\Locales;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* Base class for form elements of FormEngine. Contains several helper methods used by single elements.
*/
abstract class AbstractFormElement extends AbstractNode
{
use OnFieldChangeTrait;
/**
* Default width value for a couple of elements like text
*
* @var int
*/
protected $defaultInputWidth = 30;
/**
* Minimum width value for a couple of elements like text
*
* @var int
*/
protected $minimumInputWidth = 10;
/**
* Maximum width value for a couple of elements like text
*
* @var int
*/
protected $maxInputWidth = 50;
protected NodeFactory $nodeFactory;
/**
* Injection of NodeFactory, which is used in this abstract already.
* Using inject* method to not pollute __construct() for inheriting classes.
*/
public function injectNodeFactory(NodeFactory $nodeFactory): void
{
$this->nodeFactory = $nodeFactory;
}
/**
* Merge field information configuration with default and render them.
*
* @return array Result array
*/
protected function renderFieldInformation(): array
{
$options = $this->data;
$fieldInformation = $this->defaultFieldInformation;
$fieldInformationFromTca = $options['parameterArray']['fieldConf']['config']['fieldInformation'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldInformation, $fieldInformationFromTca);
$options['renderType'] = 'fieldInformation';
$options['renderData']['fieldInformation'] = $fieldInformation;
return $this->nodeFactory->create($options)->render();
}
/**
* Merge field control configuration with default controls and render them.
*
* @return array Result array
*/
protected function renderFieldControl(): array
{
$options = $this->data;
$fieldControl = $this->defaultFieldControl;
$fieldControlFromTca = $options['parameterArray']['fieldConf']['config']['fieldControl'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca);
$options['renderType'] = 'fieldControl';
$options['renderData']['fieldControl'] = $fieldControl;
return $this->nodeFactory->create($options)->render();
}
/**
* Merge field wizard configuration with default wizards and render them.
*
* @return array Result array
*/
protected function renderFieldWizard(): array
{
$options = $this->data;
$fieldWizard = $this->defaultFieldWizard;
$fieldWizardFromTca = $options['parameterArray']['fieldConf']['config']['fieldWizard'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca);
$options['renderType'] = 'fieldWizard';
$options['renderData']['fieldWizard'] = $fieldWizard;
return $this->nodeFactory->create($options)->render();
}
/**
* Render a label element for the current field by given id.
*/
protected function renderLabel(string $for): string
{
$label = htmlspecialchars($this->data['parameterArray']['fieldConf']['label'] ?? '');
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$fieldName = $this->data['flexFormContainerFieldName'] ?? $this->data['flexFormFieldName'] ?? $this->data['containerFieldName'] ?? $this->data['fieldName'];
$label .= ' <code>[' . htmlspecialchars($fieldName) . ']</code>';
}
$html = '<label for="' . htmlspecialchars($for) . '" class="form-label t3js-formengine-label">' . $label . '</label>';
$html .= $this->renderDescription();
return $html;
}
/**
* Elements that don't render a simple input field can't have a '<label for="..."'.
* A fieldset with a legend is used instead.
*/
protected function wrapWithFieldsetAndLegend(string $innerHTML): string
{
$legend = htmlspecialchars($this->data['parameterArray']['fieldConf']['label'] ?? '');
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$fieldName = $this->data['flexFormContainerFieldName'] ?? $this->data['flexFormFieldName'] ?? $this->data['containerFieldName'] ?? $this->data['fieldName'];
$legend .= ' <code>[' . htmlspecialchars($fieldName) . ']</code>';
}
$html = [];
$html[] = '<fieldset>';
$html[] = '<legend class="form-label t3js-formengine-label">' . $legend . '</legend>';
$html[] = $this->renderDescription();
$html[] = $innerHTML;
$html[] = '</fieldset>';
return implode(LF, $html);
}
protected function renderDescription(): string
{
$description = (string)($this->data['parameterArray']['fieldConf']['description'] ?? '');
if ($description === '') {
return '';
}
$description = $this->getLanguageService()->sL($description);
if ($description === '') {
return '';
}
return '<div class="form-description">' . nl2br(htmlspecialchars($description)) . '</div>';
}
/**
* Returns true if the "null value" checkbox should be rendered. This is used in some
* "text" based types like "text" and "input" for some renderType's.
*
* A field has eval=null set, but has no useOverridePlaceholder defined.
* Goal is to have a field that can distinct between NULL and empty string in the database.
* A checkbox and an additional hidden field will be created, both with the same name
* and prefixed with "control[active]". If the checkbox is set (value 1), the value from the casual
* input field will be written to the database. If the checkbox is not set, the hidden field
* transfers value=0 to DataHandler, the value of the input field will then be reset to NULL by the
* DataHandler at an early point in processing, so NULL will be written to DB as field value.
*
* All that only works if the field is not within flex form scope since flex forms
* can not store a "null" value or distinct it from "empty string".
*/
protected function hasNullCheckboxButNoPlaceholder(): bool
{
$hasNullCheckboxNoPlaceholder = false;
$parameterArray = $this->data['parameterArray'];
$mode = $parameterArray['fieldConf']['config']['mode'] ?? '';
if (empty($this->data['flexFormDataStructureIdentifier'])
&& ($parameterArray['fieldConf']['config']['nullable'] ?? false)
&& ($mode !== 'useOrOverridePlaceholder')
) {
$hasNullCheckboxNoPlaceholder = true;
}
return $hasNullCheckboxNoPlaceholder;
}
/**
* Returns true if the "null value" checkbox should be rendered and the placeholder
* handling is enabled. This is used in some "text" based types like "text" and
* "input" for some renderType's.
*
* A field has useOverridePlaceholder set and null in eval and is not within a flex form.
* Here, a value from a deeper DB structure can be "fetched up" as value, and can also be overridden by a
* local value. This is used in FAL, where eg. the "title" field can have the default value from sys_file_metadata,
* the title field of sys_file_reference is then set to NULL. Or the "override" checkbox is set, and a string
* or an empty string is then written to the field of sys_file_reference.
* The situation is similar to hasNullCheckboxButNoPlaceholder(), but additionally a "default" value should be shown.
* To achieve this, again a hidden control[hidden] field is added together with a checkbox with the same name
* to transfer the information whether the default value should be used or not: Checkbox checked transfers 1 as
* value in control[active], meaning the overridden value should be used.
* Additionally to the casual input field, a second field is added containing the "placeholder" value. This
* field has no name attribute and is not transferred at all. Those two are then hidden / shown depending
* on the state of the above checkbox in via JS.
*/
protected function hasNullCheckboxWithPlaceholder(): bool
{
$hasNullCheckboxWithPlaceholder = false;
$parameterArray = $this->data['parameterArray'];
$mode = $parameterArray['fieldConf']['config']['mode'] ?? '';
if (empty($this->data['flexFormDataStructureIdentifier'])
&& ($parameterArray['fieldConf']['config']['nullable'] ?? false)
&& ($mode === 'useOrOverridePlaceholder')
) {
$hasNullCheckboxWithPlaceholder = true;
}
return $hasNullCheckboxWithPlaceholder;
}
/**
* Format field content if 'format' is set to date, filesize, ..., user
*
* @param string $format Configuration for the display.
* @param ?string $itemValue The value to display
* @param array $formatOptions Format options
* @return string Formatted field value
*/
protected function formatValue($format, $itemValue, $formatOptions = []): string
{
switch ($format) {
case 'date':
if ($itemValue) {
$option = isset($formatOptions['option']) ? trim($formatOptions['option']) : '';
if ($option) {
if (isset($formatOptions['strftime']) && $formatOptions['strftime']) {
$user = $this->getBackendUser();
if ($user->user['lang'] ?? false) {
$locale = GeneralUtility::makeInstance(Locales::class)->createLocale($user->user['lang']);
} else {
$locale = new Locale();
}
$value = (new DateFormatter())->strftime($option, (int)$itemValue, $locale);
} else {
$value = date($option, (int)$itemValue);
}
} else {
$value = BackendUtility::date((int)$itemValue);
}
if (isset($formatOptions['appendAge']) && $formatOptions['appendAge']) {
$now = DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME']);
$then = DateTimeFactory::createFromTimestamp((int)$itemValue);
$age = (new DateFormatter())->formatDateInterval(
$now->diff($then),
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
);
$value .= ' (' . $age . ')';
}
} else {
$value = '';
}
$itemValue = $value;
break;
case 'datetime':
// compatibility with "eval" (type "input")
if ($itemValue !== '' && $itemValue !== null) {
$itemValue = BackendUtility::datetime((int)$itemValue);
}
break;
case 'time':
// compatibility with "eval" (type "input")
if ($itemValue !== '' && $itemValue !== null) {
$itemValue = BackendUtility::time((int)$itemValue, false);
}
break;
case 'datetimesec':
// compatibility with "eval" (type "input")
if ($itemValue !== '' && $itemValue !== null) {
$itemValue = BackendUtility::datetimesec((int)$itemValue);
}
break;
case 'timesec':
// compatibility with "eval" (type "input")
if ($itemValue !== '' && $itemValue !== null) {
$itemValue = BackendUtility::time((int)$itemValue);
}
break;
case 'int':
$baseArr = ['dec' => 'd', 'hex' => 'x', 'HEX' => 'X', 'oct' => 'o', 'bin' => 'b'];
$base = isset($formatOptions['base']) ? trim($formatOptions['base']) : '';
$format = $baseArr[$base] ?? 'd';
$itemValue = sprintf('%' . $format, $itemValue);
break;
case 'float':
// default precision
$precision = 2;
if (isset($formatOptions['precision'])) {
$precision = MathUtility::forceIntegerInRange($formatOptions['precision'], 1, 10, $precision);
}
$itemValue = sprintf('%.' . $precision . 'f', $itemValue);
break;
case 'number':
$format = isset($formatOptions['option']) ? '%' . trim($formatOptions['option']) : '';
$itemValue = sprintf($format, $itemValue);
break;
case 'md5':
$itemValue = md5($itemValue);
break;
case 'filesize':
// We need to cast to int here, otherwise empty values result in empty output,
// but we expect zero.
$value = GeneralUtility::formatSize((int)$itemValue);
if (!empty($formatOptions['appendByteSize'])) {
$value .= ' (' . $itemValue . ')';
}
$itemValue = $value;
break;
case 'user':
$func = trim($formatOptions['userFunc']);
if ($func) {
$params = [
'value' => $itemValue,
'args' => $formatOptions['userFunc'],
'config' => [
'type' => 'none',
'format' => $format,
'format.' => $formatOptions,
],
];
$itemValue = GeneralUtility::callUserFunction($func, $params, $this);
}
break;
default:
// Do nothing e.g. when $format === ''
}
// Make sure we have a string in the end. $itemValue could be null, for instance.
return (string)$itemValue;
}
/**
* Returns the max width in pixels for an elements like input and text
*
* @param int $size The abstract size value (1-48)
* @return int Maximum width in pixels
*/
protected function formMaxWidth($size = 48)
{
$compensationForLargeDocuments = 1.33;
$compensationForFormFields = 12;
$compensatedSize = round($size * $compensationForLargeDocuments);
return (int)ceil($compensatedSize * $compensationForFormFields);
}
/**
* Handle custom javascript `eval` implementations. $evalObject is a hook object
* for custom eval's. It is transferred to JS as a JavaScriptModuleInstruction if possible.
* This is used by a couple of renderType's like various type="input", should
* be used with care and is internal for now.
*
* @internal
*/
protected function resolveJavaScriptEvaluation(array $resultArray, string $name, ?object $evalObject): array
{
if (!is_object($evalObject) || !method_exists($evalObject, 'returnFieldJS')) {
return $resultArray;
}
$javaScriptEvaluation = $evalObject->returnFieldJS();
if ($javaScriptEvaluation instanceof JavaScriptModuleInstruction) {
// just use the module name and export-name
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
$javaScriptEvaluation->getName(),
$javaScriptEvaluation->getExportName()
)->invoke('registerCustomEvaluation', $name);
}
return $resultArray;
}
/***********************************************
* CheckboxElement related methods
***********************************************/
/**
* Creates checkbox parameters
*
* @param string $itemName Form element name
* @param int $formElementValue The value of the checkbox (representing checkboxes with the bits)
* @param int $checkbox Checkbox # (0-9?)
* @param int $checkboxesCount Total number of checkboxes in the array.
* @param array $fieldChangeFuncs `fieldChangeFunc` items for client-side handling
* @param bool $invert Inverts the state of the checkbox (but not of the bit value)
* @return string either `onclick` attr or `data-formengine-field-change-*` attrs + possibly the checked-option set
* @internal
*/
protected function checkBoxParams(
string $itemName,
int $formElementValue,
int $checkbox,
int $checkboxesCount,
array $fieldChangeFuncs = [],
bool $invert = false
): string {
array_unshift($fieldChangeFuncs, new UpdateBitmaskOnFieldChange(
$checkbox,
$checkboxesCount,
$invert,
$itemName
));
$checkboxPow = 2 ** $checkbox;
$checked = $formElementValue & $checkboxPow;
$attrs = $this->getOnFieldChangeAttrs('click', $fieldChangeFuncs);
if ($checked xor $invert) {
$attrs['checked'] = 'checked';
}
$attrs['data-invert-state-display'] = $invert ? 'true' : 'false';
return GeneralUtility::implodeAttributes($attrs, true);
}
/**
* Append the value of a form field to its label
*/
protected function appendValueToLabelInDebugMode(string|int $label, string|int $value): string
{
if ($value !== '' && $this->getBackendUser()->shallDisplayDebugInformation()) {
return trim($label . ' [' . $value . ']');
}
return trim((string)$label);
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
protected function getBackendUser(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
+85
View File
@@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Backend\Avatar\DefaultAvatarProvider;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Renders an avatar element for user settings.
*/
class AvatarElement extends AbstractFormElement
{
public function __construct(
private readonly IconFactory $iconFactory,
private readonly UriBuilder $uriBuilder
) {}
public function render(): array
{
$resultArray = $this->initializeResultArray();
$fieldName = $this->data['fieldName'];
$irreObjectId = '-0-be_users-avatar-' . $fieldName;
$parameterArray = $this->data['parameterArray'];
$defaultAvatarProvider = GeneralUtility::makeInstance(DefaultAvatarProvider::class);
$avatarImage = $defaultAvatarProvider->getImage($this->data['databaseRow'], 32);
$html = '';
if ($avatarImage) {
$icon = '<span class="avatar avatar-size-medium mb-2"><span class="avatar-image">'
. '<img alt="" src="' . htmlspecialchars($avatarImage->getUrl()) . '"'
. ' width="' . (int)$avatarImage->getWidth() . '"'
. ' height="' . (int)$avatarImage->getHeight() . '"'
. ' alt="" />'
. '</span></span>';
$html .= '<span id="image_' . htmlspecialchars($fieldName) . '">' . $icon . ' </span>';
}
$html .= '<input id="field_' . htmlspecialchars($fieldName) . '" type="hidden" '
. 'name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"'
. ' value="' . (int)($this->data['databaseRow']['avatar'] ?? 0) . '" data-setup-avatar-field="' . htmlspecialchars($fieldName) . '" />';
$html .= '<div class="form-group"><div class="form-group"><div class="form-control-wrap">';
$html .= '<button type="button" id="add_button_' . htmlspecialchars($fieldName)
. '" class="btn btn-default"'
. ' title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/user_profile.xlf:avatar.open_file_browser')) . '"'
. ' data-setup-avatar-url="' . htmlspecialchars((string)$this->uriBuilder->buildUriFromRoute('wizard_element_browser', ['mode' => 'file', 'allowedTypes' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] ?? '', 'irreObjectId' => $irreObjectId])) . '"'
. '>' . $this->iconFactory->getIcon('actions-insert-record', IconSize::SMALL)->render()
. htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/user_profile.xlf:avatar.open_file_browser'))
. '</button>';
if ($avatarImage) {
$html .= ' ';
$html .= '<button type="button" id="clear_button_' . htmlspecialchars($fieldName)
. '" class="btn btn-default"'
. ' title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/user_profile.xlf:avatar.clear')) . '" '
. '>' . $this->iconFactory->getIcon('actions-delete', IconSize::SMALL)->render()
. htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/user_profile.xlf:avatar.clear'))
. '</button>';
}
$html .= '</div></div></div>';
$resultArray['html'] = $html;
return $resultArray;
}
}
@@ -0,0 +1,214 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\EventDispatcher\NoopEventDispatcher;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\TypoScript\AST\AstBuilder;
use TYPO3\CMS\Core\TypoScript\TypoScriptStringFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Backend layout element. This is used when editing backend_layout records.
* It renders the layout wizard to manage rows and columns and shows the pseudo TypoScript result.
*
* Note this element does not support fancy TypoScript features like @import
* lines and special ":=" value manipulation functions. When backend_layouts want to use
* these, they shouldn't use table record based backend_layouts, but register backend layouts
* using the BackendLayout/DataProviderInterface to store them in files, which obsoletes
* table record based backend_layouts and with it this FormEngine element class.
*
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class BackendLayoutWizardElement extends AbstractFormElement
{
protected array $rows = [];
protected int $colCount = 0;
protected int $rowCount = 0;
public function __construct(
private readonly TypoScriptStringFactory $typoScriptStringFactory,
) {}
public function render(): array
{
$lang = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$this->initializeWizard();
$row = $this->data['databaseRow'];
$tca = $this->data['processedTca'];
$parameterArray = $this->data['parameterArray'];
// readOnly is not supported as columns config but might be set by SingleFieldContainer in case
// "l10n_display" is set to "defaultAsReadonly". To prevent misbehaviour for fields, which falsely
// set this, we also check for "defaultAsReadonly" being set and whether the record is an overlay.
$readOnly = ($parameterArray['fieldConf']['config']['readOnly'] ?? false)
&& ($tca['ctrl']['transOrigPointerField'] ?? false)
&& ($row[$tca['ctrl']['transOrigPointerField']][0] ?? $row[$tca['ctrl']['transOrigPointerField']] ?? false)
&& GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'] ?? '', 'defaultAsReadonly');
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
// Use CodeMirror if available
$codeMirrorConfig = [
'label' => $lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:buttons.pageTsConfig'),
'panel' => 'top',
'mode' => GeneralUtility::jsonEncodeForHtmlAttribute(JavaScriptModuleInstruction::create('@typo3/backend/code-editor/language/typoscript.js', 'typoscript')->invoke(), false),
'nolazyload' => 'true',
'readonly' => 'true',
];
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/code-editor/element/code-mirror-element.js');
$json = (string)json_encode($this->rows, JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS);
$codeMirrorConfig = (string)json_encode($codeMirrorConfig, JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<input';
$html[] = ' type="hidden"';
$html[] = ' name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '"';
$html[] = ' value="' . htmlspecialchars($this->data['parameterArray']['itemFormElValue']) . '"';
$html[] = '/>';
$html[] = '<typo3-backend-grid-editor';
$html[] = ' data="' . htmlspecialchars($json) . '"';
$html[] = ' rowCount="' . (int)$this->rowCount . '"';
$html[] = ' colCount="' . (int)$this->colCount . '"';
$html[] = ($readOnly ? 'readonly="true"' : '');
$html[] = ' fieldName="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '"';
$html[] = ' codeMirrorConfig="' . htmlspecialchars($codeMirrorConfig) . '"';
$html[] = '></typo3-backend-grid-editor>';
if (!$readOnly && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">' . $fieldWizardHtml . '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html = implode(LF, $html);
$resultArray['html'] = $this->wrapWithFieldsetAndLegend($html);
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/grid-editor.js',
'GridEditor'
)->instance();
$resultArray['additionalInlineLanguageLabelFiles'][] = 'EXT:core/Resources/Private/Language/locallang_wizards.xlf';
$resultArray['additionalInlineLanguageLabelFiles'][] = 'EXT:backend/Resources/Private/Language/locallang.xlf';
$resultArray['additionalInlineLanguageLabelFiles'][] = 'EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf';
return $resultArray;
}
protected function initializeWizard(): void
{
// Initialize default values
$rows = [[['colspan' => 1, 'rowspan' => 1, 'spanned' => 0, 'name' => '0x0']]];
$colCount = 1;
$rowCount = 1;
if (!empty($this->data['parameterArray']['itemFormElValue'])) {
// Parse the TypoScript a-like syntax in case we already have a config (e.g. database value or default from TCA)
$typoScriptTree = $this->typoScriptStringFactory->parseFromString($this->data['parameterArray']['itemFormElValue'], new AstBuilder(new NoopEventDispatcher()));
$typoScriptArray = $typoScriptTree->toArray();
if (is_array($typoScriptArray['backend_layout.'] ?? false)) {
// Only evaluate, in case the "backend_layout." array exists on root level
$data = $typoScriptArray['backend_layout.'];
$rows = [];
$colCount = $data['colCount'];
$rowCount = $data['rowCount'];
$dataRows = $data['rows.'];
$spannedMatrix = [];
for ($i = 1; $i <= $rowCount; $i++) {
$cells = [];
$row = array_shift($dataRows);
$columns = $row['columns.'] ?? [];
for ($j = 1; $j <= $colCount; $j++) {
$cellData = [];
if (!($spannedMatrix[$i][$j] ?? false)) {
if (is_array($columns) && !empty($columns)) {
$column = array_shift($columns);
if (isset($column['colspan'])) {
$cellData['colspan'] = (int)$column['colspan'];
$columnColSpan = (int)$column['colspan'];
if (isset($column['rowspan'])) {
$columnRowSpan = (int)$column['rowspan'];
for ($spanRow = 0; $spanRow < $columnRowSpan; $spanRow++) {
for ($spanColumn = 0; $spanColumn < $columnColSpan; $spanColumn++) {
$spannedMatrix[$i + $spanRow][$j + $spanColumn] = 1;
}
}
} else {
for ($spanColumn = 0; $spanColumn < $columnColSpan; $spanColumn++) {
$spannedMatrix[$i][$j + $spanColumn] = 1;
}
}
} else {
$cellData['colspan'] = 1;
if (isset($column['rowspan'])) {
$columnRowSpan = (int)$column['rowspan'];
for ($spanRow = 0; $spanRow < $columnRowSpan; $spanRow++) {
$spannedMatrix[$i + $spanRow][$j] = 1;
}
}
}
if (isset($column['rowspan'])) {
$cellData['rowspan'] = (int)$column['rowspan'];
} else {
$cellData['rowspan'] = 1;
}
if (isset($column['name'])) {
$cellData['name'] = $column['name'];
}
if (isset($column['colPos'])) {
$cellData['column'] = (int)$column['colPos'];
}
if (isset($column['identifier'])) {
$cellData['identifier'] = $column['identifier'];
}
if (isset($column['slideMode'])) {
$cellData['slideMode'] = $column['slideMode'];
}
}
} else {
$cellData = ['colspan' => 1, 'rowspan' => 1, 'spanned' => 1];
}
$cells[] = $cellData;
}
$rows[] = $cells;
if (is_array($spannedMatrix[$i] ?? false) && $spannedMatrix[$i] !== []) {
ksort($spannedMatrix[$i]);
}
}
}
}
$this->rows = $rows;
$this->colCount = (int)$colCount;
$this->rowCount = (int)$rowCount;
}
}
+157
View File
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render the category element (a category "select" tree).
*/
class CategoryElement extends AbstractFormElement
{
private const int MIN_ITEMS_COUNT = 5;
private const int DEFAULT_ITEMS_COUNT = 15;
private const int ITEM_HEIGHT_BASE = 20;
/**
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
];
/**
* Render the category tree
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$fieldName = $this->data['fieldName'];
$tableName = $this->data['tableName'];
$parameterArray = $this->data['parameterArray'];
$formElementId = md5($parameterArray['itemFormElName']);
// Field configuration from TCA:
$config = $parameterArray['fieldConf']['config'];
$readOnly = (bool)($config['readOnly'] ?? false);
$expanded = (bool)($config['treeConfig']['appearance']['expandAll'] ?? false);
$showHeader = (bool)($config['treeConfig']['appearance']['showHeader'] ?? false);
$exclusiveKeys = $config['exclusiveKeys'] ?? '';
$height = ((int)($config['size'] ?? 0) > 0)
? max(self::MIN_ITEMS_COUNT, (int)$config['size'])
: self::DEFAULT_ITEMS_COUNT;
$heightInPx = $height * self::ITEM_HEIGHT_BASE;
$treeWrapperId = 'tree_' . $formElementId;
$fieldId = 'tree_record_' . $formElementId;
$dataStructureIdentifier = '';
$flexFormSheetName = '';
$flexFormFieldName = '';
$flexFormContainerName = '';
$flexFormContainerIdentifier = '';
$flexFormContainerFieldName = '';
$flexFormSectionContainerIsNew = false;
if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
$dataStructureIdentifier = $this->data['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'];
if (isset($this->data['flexFormSheetName'])) {
$flexFormSheetName = $this->data['flexFormSheetName'];
}
if (isset($this->data['flexFormFieldName'])) {
$flexFormFieldName = $this->data['flexFormFieldName'];
}
if (isset($this->data['flexFormContainerName'])) {
$flexFormContainerName = $this->data['flexFormContainerName'];
}
if (isset($this->data['flexFormContainerFieldName'])) {
$flexFormContainerFieldName = $this->data['flexFormContainerFieldName'];
}
if (isset($this->data['flexFormContainerIdentifier'])) {
$flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier'];
}
// Add a flag this is a tree in a new flex section container element. This is needed to initialize
// the databaseRow with this container again so the tree data provider is able to calculate tree items.
if (!empty($this->data['flexSectionContainerPreparation'])) {
$flexFormSectionContainerIsNew = true;
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
if (!$readOnly && !empty($fieldWizardHtml)) {
$fieldWizardHtml = '<div class="form-wizards-item-bottom">' . $fieldWizardHtml . '</div>';
}
$recordElementAttributes = [
'id' => $fieldId,
'type' => 'hidden',
'class' => 'treeRecord',
'name' => $parameterArray['itemFormElName'],
'value' => implode(',', $parameterArray['itemFormElValue']),
'data-uid' => (int)$this->data['vanillaUid'],
'data-command' => $this->data['command'],
'data-fieldname' => $fieldName,
'data-tablename' => $tableName,
'data-read-only' => $readOnly,
'data-tree-show-toolbar' => $showHeader,
'data-recordtypevalue' => $this->data['recordTypeValue'],
'data-relatedfieldname' => $parameterArray['itemFormElName'],
'data-flexformsheetname' => $flexFormSheetName,
'data-flexformfieldname' => $flexFormFieldName,
'data-tree-exclusive-keys' => $exclusiveKeys,
'data-flexformcontainername' => $flexFormContainerName,
'data-datastructureidentifier' => $dataStructureIdentifier,
'data-tree-expand-up-to-level' => $expanded ? '999' : '1',
'data-flexformcontainerfieldname' => $flexFormContainerFieldName,
'data-flexformcontaineridentifier' => $flexFormContainerIdentifier,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-flexformsectioncontainerisnew' => (string)$flexFormSectionContainerIsNew,
'data-overridevalues' => GeneralUtility::jsonEncodeForHtmlAttribute($this->data['overrideValues'], false),
'data-defaultvalues' => GeneralUtility::jsonEncodeForHtmlAttribute($this->data['defaultValues'], false),
];
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(
'<typo3-formengine-element-category ' . GeneralUtility::implodeAttributes(['class' => 'formengine-field-item t3js-formengine-field-item', 'recordFieldId' => $fieldId, 'treeWrapperId' => $treeWrapperId], true) . '>
' . $fieldInformationHtml . '
<div class="form-control-wrap">
<div class="form-wizards-wrap">
<div class="form-wizards-item-element">
<div class="typo3-tceforms-tree">
<input ' . GeneralUtility::implodeAttributes(array_map(strval(...), $recordElementAttributes), true, true) . '/>
</div>
<div id="' . htmlspecialchars($treeWrapperId) . '" class="tree-element" style="height: ' . $heightInPx . 'px;"></div>
</div>
' . $fieldWizardHtml . '
</div>
</div>
</typo3-formengine-element-category>'
);
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/category-element.js');
return $resultArray;
}
}
+185
View File
@@ -0,0 +1,185 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render elements of type="check".
*/
class CheckboxElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
private readonly IconRegistry $iconRegistry,
) {}
/**
* This will render a checkbox or an array of checkboxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$elementHtml = '';
$disabled = false;
if ($this->data['parameterArray']['fieldConf']['config']['readOnly'] ?? false) {
$disabled = true;
}
// Traversing the array of items
$items = $this->data['parameterArray']['fieldConf']['config']['items'] ?? [];
$numberOfItems = count($items);
if ($numberOfItems === 0) {
$items[] = ['label' => ''];
$numberOfItems = 1;
}
$formElementValue = (int)$this->data['parameterArray']['itemFormElValue'];
$cols = (int)($this->data['parameterArray']['fieldConf']['config']['cols'] ?? 0);
if ($cols > 1) {
$elementHtml .= '<div class="form-grid" style="--typo3-form-grid-columns: ' . $cols . ';">';
// $itemKey is important here, because items could have been removed via TSConfig
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition['label'];
$elementHtml
.= '<div class="form-group">'
. $this->renderSingleCheckboxElement($label, $itemKey, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled)
. '</div>';
}
$elementHtml .= '</div>';
} else {
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition['label'];
$elementHtml .= $this->renderSingleCheckboxElement($label, $itemKey, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled);
}
}
if (!$disabled) {
$elementHtml .= '<input type="hidden" name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '" value="' . htmlspecialchars((string)$formElementValue) . '" />';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = $elementHtml;
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
/**
* This functions builds the HTML output for the checkbox
*
* @param string $label Label of this item
* @param int $itemCounter Number of this element in the list of all elements
* @param int $formElementValue Value of this element
* @param int $numberOfItems Full number of items
* @param array $additionalInformation Information with additional configuration options.
* @param bool $disabled TRUE if form element is disabled
* @return string Single element HTML
*/
protected function renderSingleCheckboxElement($label, $itemCounter, $formElementValue, $numberOfItems, $additionalInformation, $disabled): string
{
$config = $additionalInformation['fieldConf']['config'];
$inline = !empty($config['cols']) && $config['cols'] === 'inline';
$invert = isset($config['items'][$itemCounter]['invertStateDisplay']) && $config['items'][$itemCounter]['invertStateDisplay'] === true;
$checkboxParameters = $this->checkBoxParams(
$additionalInformation['itemFormElName'],
$formElementValue,
$itemCounter,
$numberOfItems,
$additionalInformation['fieldChangeFunc'] ?? [],
$invert
);
$checkboxId = htmlspecialchars(StringUtility::getUniqueId('formengine-check-') . '-' . $itemCounter);
$iconIdentifierChecked = !empty($config['items'][$itemCounter]['iconIdentifierChecked']) ? $config['items'][$itemCounter]['iconIdentifierChecked'] : 'actions-check';
if (!$this->iconRegistry->isRegistered($iconIdentifierChecked)) {
$iconIdentifierChecked = 'actions-check';
}
$iconIdentifierUnchecked = !empty($config['items'][$itemCounter]['iconIdentifierUnchecked']) ? $config['items'][$itemCounter]['iconIdentifierUnchecked'] : 'empty-empty';
if (!$this->iconRegistry->isRegistered($iconIdentifierUnchecked)) {
$iconIdentifierUnchecked = 'empty-empty';
}
$iconChecked = $this->iconFactory->getIcon($iconIdentifierChecked, IconSize::SMALL)->render('inline');
$iconUnchecked = $this->iconFactory->getIcon($iconIdentifierUnchecked, IconSize::SMALL)->render('inline');
return '
<div class="form-check form-check-type-icon-toggle' . ($inline ? ' form-check-inline' : '') . (!$disabled ? '' : ' disabled') . '">
<input type="checkbox"
class="form-check-input"
value="1"
data-formengine-input-name="' . htmlspecialchars($additionalInformation['itemFormElName']) . '"
' . $checkboxParameters . '
' . ($disabled ? ' disabled="disabled"' : '') . '
id="' . $checkboxId . '" />
<label class="form-check-label" for="' . $checkboxId . '">
<span class="form-check-label-icon">
<span class="form-check-label-icon-checked">' . $iconChecked . '</span>
<span class="form-check-label-icon-unchecked">' . $iconUnchecked . '</span>
</span>
' . $this->appendValueToLabelInDebugMode(($label ? htmlspecialchars($label) : ''), $formElementValue) . '
</label>
</div>';
}
}
@@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render elements of TCA type="check" with renderType="checkboxLabeledToggle".
*/
class CheckboxLabeledToggleElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a checkbox or an array of checkboxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$elementHtml = '';
$disabled = false;
if ($this->data['parameterArray']['fieldConf']['config']['readOnly'] ?? false) {
$disabled = true;
}
// Traversing the array of items
$items = $this->data['parameterArray']['fieldConf']['config']['items'];
$numberOfItems = count($items);
if ($numberOfItems === 0) {
$items[] = ['label' => ''];
$numberOfItems = 1;
}
$formElementValue = (int)$this->data['parameterArray']['itemFormElValue'];
$cols = (int)($this->data['parameterArray']['fieldConf']['config']['cols'] ?? 0);
if ($cols > 1) {
$elementHtml .= '<div class="form-grid" style="--typo3-form-grid-columns: ' . $cols . ';">';
// $itemKey is important here, because items could have been removed via TSConfig
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition['label'];
$elementHtml
.= '<div class="form-group">'
. $this->renderSingleCheckboxElement($label, $itemKey, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled)
. '</div>';
}
$elementHtml .= '</div>';
} else {
$counter = 0;
foreach ($items as $itemDefinition) {
$label = $itemDefinition['label'];
$elementHtml .= $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled);
++$counter;
}
}
if (!$disabled) {
$elementHtml .= '<input type="hidden" name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '" value="' . htmlspecialchars((string)$formElementValue) . '" />';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = $elementHtml;
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
/**
* This functions builds the HTML output for the checkbox
*
* @param string $label Label of this item
* @param int $itemCounter Number of this element in the list of all elements
* @param int $formElementValue Value of this element
* @param int $numberOfItems Full number of items
* @param array $additionalInformation Information with additional configuration options.
* @param bool $disabled TRUE if form element is disabled
* @return string Single element HTML
*/
protected function renderSingleCheckboxElement($label, $itemCounter, $formElementValue, $numberOfItems, $additionalInformation, $disabled): string
{
$config = $additionalInformation['fieldConf']['config'];
$inline = !empty($config['cols']) && $config['cols'] === 'inline';
$invert = isset($config['items'][$itemCounter]['invertStateDisplay']) && $config['items'][$itemCounter]['invertStateDisplay'] === true;
$checkboxParameters = $this->checkBoxParams(
$additionalInformation['itemFormElName'],
$formElementValue,
$itemCounter,
$numberOfItems,
$additionalInformation['fieldChangeFunc'] ?? [],
$invert
);
$checkboxId = htmlspecialchars(StringUtility::getUniqueId('formengine-check-labeled-') . '-' . $itemCounter);
return '
<div class="form-check form-check-type-labeled-toggle' . ($inline ? ' form-check-inline' : '') . (!$disabled ? '' : ' disabled') . '">
<input type="checkbox"
class="form-check-input"
value="1"
data-form-check-label-checked="' . $config['items'][$itemCounter]['labelChecked'] . '"
data-form-check-label-unchecked="' . $config['items'][$itemCounter]['labelUnchecked'] . '"
data-formengine-input-name="' . htmlspecialchars($additionalInformation['itemFormElName']) . '"
' . $checkboxParameters . '
' . (!$disabled ? '' : ' disabled="disabled"') . '
id="' . $checkboxId . '" />
<label class="form-check-label" for="' . $checkboxId . '">
' . $this->appendValueToLabelInDebugMode(($label ? htmlspecialchars($label) : ''), $formElementValue) . '
</label>
</div>';
}
}
@@ -0,0 +1,167 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render elements of TCA type="check" with renderType="checkboxToggle".
*/
class CheckboxToggleElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a checkbox or an array of checkboxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$elementHtml = '';
$disabled = false;
if ($this->data['parameterArray']['fieldConf']['config']['readOnly'] ?? false) {
$disabled = true;
}
// Traversing the array of items
$items = $this->data['parameterArray']['fieldConf']['config']['items'] ?? [];
$numberOfItems = count($items);
if ($numberOfItems === 0) {
$items[] = ['label' => ''];
$numberOfItems = 1;
}
// The values in the array may be numeric strings, but we need real ints.
$formElementValue = (int)($this->data['parameterArray']['itemFormElValue'] ?? 0);
$cols = (int)($this->data['parameterArray']['fieldConf']['config']['cols'] ?? 0);
if ($cols > 1) {
$elementHtml .= '<div class="form-grid" style="--typo3-form-grid-columns: ' . $cols . ';">';
// $itemKey is important here, because items could have been removed via TSConfig
foreach ($items as $itemKey => $itemDefinition) {
$label = $itemDefinition['label'];
$elementHtml
.= '<div class="form-group">'
. $this->renderSingleCheckboxElement($label, $itemKey, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled)
. '</div>';
}
$elementHtml .= '</div>';
} else {
$counter = 0;
foreach ($items as $itemDefinition) {
$label = $itemDefinition['label'];
$elementHtml .= $this->renderSingleCheckboxElement($label, $counter, $formElementValue, $numberOfItems, $this->data['parameterArray'], $disabled);
++$counter;
}
}
if (!$disabled) {
$elementHtml .= '<input type="hidden" name="' . htmlspecialchars($this->data['parameterArray']['itemFormElName']) . '" value="' . htmlspecialchars((string)$formElementValue) . '" />';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = $elementHtml;
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
/**
* This functions builds the HTML output for the checkbox
*
* @param string $label Label of this item
* @param int $itemCounter Number of this element in the list of all elements
* @param int $formElementValue Value of this element
* @param int $numberOfItems Full number of items
* @param array $additionalInformation Information with additional configuration options
* @param bool $disabled TRUE if form element is disabled
* @return string Single element HTML
*/
protected function renderSingleCheckboxElement($label, $itemCounter, $formElementValue, $numberOfItems, $additionalInformation, $disabled): string
{
$languageService = $this->getLanguageService();
$parentLabel = $additionalInformation['fieldConf']['label'] ?? '';
$config = $additionalInformation['fieldConf']['config'];
$inline = ($config['cols'] ?? '') === 'inline';
$invert = isset($config['items'][0]['invertStateDisplay']) && $config['items'][0]['invertStateDisplay'] === true;
$checkboxParameters = $this->checkBoxParams(
$additionalInformation['itemFormElName'],
$formElementValue,
$itemCounter,
$numberOfItems,
$additionalInformation['fieldChangeFunc'] ?? [],
$invert
);
$checkboxId = htmlspecialchars(StringUtility::getUniqueId('formengine-check-toggle-') . '-' . $itemCounter);
return '
<div class="form-check form-switch' . ($inline ? ' form-check-inline' : '') . (!$disabled ? '' : ' disabled') . '">
<input type="checkbox"
class="form-check-input"
value="1"
role="switch"
data-formengine-input-name="' . htmlspecialchars($additionalInformation['itemFormElName']) . '"
' . $checkboxParameters . '
' . (!$disabled ? '' : ' disabled="disabled"') . '
id="' . $checkboxId . '" />
<label class="form-check-label" for="' . $checkboxId . '">
' . $this->appendValueToLabelInDebugMode(($label ? htmlspecialchars($label) : ''), $formElementValue) . '<span class="visually-hidden">' . htmlspecialchars($parentLabel) . '</span>' . '
</label>
</div>';
}
}
+238
View File
@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\CodeEditor\CodeEditor;
use TYPO3\CMS\Backend\CodeEditor\Exception\InvalidModeException;
use TYPO3\CMS\Backend\CodeEditor\Mode;
use TYPO3\CMS\Backend\CodeEditor\Registry\AddonRegistry;
use TYPO3\CMS\Backend\CodeEditor\Registry\ModeRegistry;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
/**
* CodeEditor FormEngine widget
* @internal
*/
class CodeEditorElement extends AbstractFormElement
{
protected array $resultArray = [];
protected string $mode = '';
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* Render code editor element
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws InvalidModeException
* @throws \InvalidArgumentException
* @throws \BadFunctionCallException
*/
public function render(): array
{
$this->resultArray = $this->initializeResultArray();
$this->resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/code-editor/element/code-mirror-element.js');
// Compile and register code editor configuration
GeneralUtility::makeInstance(CodeEditor::class)->registerConfiguration();
$addonRegistry = GeneralUtility::makeInstance(AddonRegistry::class);
$registeredAddons = $addonRegistry->getAddons();
foreach ($registeredAddons as $addon) {
foreach ($addon->getCssFiles() as $cssFile) {
$this->resultArray['stylesheetFiles'][] = $cssFile;
}
}
$parameterArray = $this->data['parameterArray'];
$attributes = [
'wrap' => 'off',
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($parameterArray['fieldConf']['config']),
];
if (isset($parameterArray['fieldConf']['config']['rows']) && MathUtility::canBeInterpretedAsInteger($parameterArray['fieldConf']['config']['rows'])) {
$attributes['rows'] = $parameterArray['fieldConf']['config']['rows'];
}
$settings = [];
if ($parameterArray['fieldConf']['config']['readOnly'] ?? false) {
$settings['readonly'] = true;
}
if ($parameterArray['fieldConf']['config']['appearance']['lineWrapping'] ?? false) {
$settings['linewrapping'] = true;
}
$editorHtml = $this->getHTMLCodeForEditor(
$parameterArray['itemFormElName'],
'form-control font-monospace enable-tab',
$parameterArray['itemFormElValue'],
$attributes,
$settings,
[
'target' => 0,
'effectivePid' => $this->data['effectivePid'] ?? 0,
]
);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = $editorHtml;
$html[] = '</div>';
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = '<div class="btn-group">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$this->resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $this->resultArray;
}
/**
* Generates HTML with code editor
*
* @param string $name Name attribute of HTML tag
* @param string $class Class attribute of HTML tag
* @param string $content Content of the editor
* @param array $attributes Any additional editor parameters
*
* @return string Generated HTML code for editor
* @throws \TYPO3\CMS\Backend\CodeEditor\Exception\InvalidModeException
*/
protected function getHTMLCodeForEditor(
string $name,
string $class = '',
string $content = '',
array $attributes = [],
array $settings = [],
array $hiddenfields = []
): string {
$code = [];
$mode = $this->getMode();
$addonRegistry = GeneralUtility::makeInstance(AddonRegistry::class);
$registeredAddons = $addonRegistry->getAddons();
$attributes['class'] = $class;
$attributes['id'] = 't3editor_' . md5($name);
$attributes['name'] = $name;
$settings = array_merge($addonRegistry->compileSettings($registeredAddons), $settings);
$addons = [];
$keymaps = [];
foreach ($registeredAddons as $addon) {
$module = $addon->getModule();
$keymap = $addon->getKeymap();
if ($module) {
$addons[] = $module;
}
if ($keymap) {
$keymaps[] = $keymap;
}
}
$codeMirrorConfig = array_merge($settings, [
'name' => $name,
'mode' => GeneralUtility::jsonEncodeForHtmlAttribute($mode->getModule(), false),
'addons' => GeneralUtility::jsonEncodeForHtmlAttribute($addons, false),
'keymaps' => GeneralUtility::jsonEncodeForHtmlAttribute($keymaps, false),
]);
$code[] = '<typo3-t3editor-codemirror ' . GeneralUtility::implodeAttributes($codeMirrorConfig, true) . '>';
$code[] = GeneralUtility::renderTextarea($content, $attributes);
if (!empty($hiddenfields)) {
foreach ($hiddenfields as $attributeName => $value) {
$code[] = '<input type="hidden" name="' . htmlspecialchars((string)$attributeName) . '" value="' . htmlspecialchars((string)$value) . '" />';
}
}
$code[] = '</typo3-t3editor-codemirror>';
return implode(LF, $code);
}
/**
* @throws InvalidModeException
*/
protected function getMode(): Mode
{
$config = $this->data['parameterArray']['fieldConf']['config'];
$registry = GeneralUtility::makeInstance(ModeRegistry::class);
if (!isset($config['format'])) {
return $registry->getDefaultMode();
}
$identifier = $config['format'];
if (str_contains($config['format'], '/')) {
$parts = explode('/', $config['format']);
$identifier = end($parts);
}
return $registry->getByFormatCode($identifier);
}
}
+284
View File
@@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render an input field with a color picker
*/
class ColorElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$tsConfig = $this->data['pageTsConfig'];
$itemValue = $parameterArray['itemFormElValue'];
$width = $this->formMaxWidth(
MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth)
);
$fieldId = StringUtility::getUniqueId('formengine-input-');
$itemName = (string)$parameterArray['itemFormElName'];
$renderedLabel = $this->renderLabel($fieldId);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<typo3-backend-color-picker>';
$html[] = '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" name="' . htmlspecialchars($itemName) . '" value="' . htmlspecialchars((string)$itemValue) . '" type="text" disabled>';
$html[] = '</typo3-backend-color-picker>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$languageService = $this->getLanguageService();
// Always add "trim".
$evalList = ['trim'];
if ($config['nullable'] ?? false) {
$evalList[] = 'null';
}
$opacityEnabled = (bool)($config['opacity'] ?? false);
$attributes = [
'value' => '',
'id' => $fieldId,
'class' => implode(' ', [
'form-control',
]),
'maxlength' => $opacityEnabled ? 9 : 7, // #RRGGBBAA (/#[0-9a-fA-F]{3,6}([0-9]{2})?/)
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => (string)json_encode([
'field' => $itemName,
'evalList' => implode(',', $evalList),
], JSON_THROW_ON_ERROR),
'data-formengine-input-name' => $itemName,
];
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$colorDefinitions = array_map(
fn(array $colorDefinition): array => [
'color' => $colorDefinition['value'],
'label' => $this->resolveColorLabel($colorDefinition['value'], $colorDefinition['label'] ?? null),
],
array_filter(
$tsConfig['colorPalettes.']['colors.'] ?? [],
static fn(mixed $colorDefinition) => is_array($colorDefinition) && trim($colorDefinition['value'] ?? '') !== '',
),
);
$configuredPalette
= $tsConfig['TCEFORM.'][$table . '.'][$fieldName . '.']['colorPalette']
?? $tsConfig['TCEFORM.'][$table . '.']['colorPalette']
?? $tsConfig['TCEFORM.']['colorPalette']
?? null;
if ($configuredPalette !== null) {
$colorsInPalette = GeneralUtility::trimExplode(',', $tsConfig['colorPalettes.']['palettes.'][$configuredPalette] ?? '', true);
$colorDefinitions = array_map(
static fn(string $colorIdentifier): array => $colorDefinitions[$colorIdentifier . '.'],
array_filter(
array_combine($colorsInPalette, $colorsInPalette),
static fn(string $colorIdentifier) => isset($colorDefinitions[$colorIdentifier . '.'])
)
);
}
if (is_array($config['valuePicker']['items'] ?? false)) {
foreach ($config['valuePicker']['items'] as $item) {
$colorDefinitions[] = [
'color' => $item['value'],
'label' => $this->resolveColorLabel($item['value'], $item['label'] ?? null),
];
}
}
$colorPickerAttribute = [
'swatches' => json_encode(array_values($colorDefinitions)),
'color' => htmlspecialchars((string)$itemValue),
];
if ($opacityEnabled) {
$colorPickerAttribute['opacity'] = 'true';
}
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-item-element">';
$mainFieldHtml[] = '<typo3-backend-color-picker ' . GeneralUtility::implodeAttributes($colorPickerAttribute, true) . '>';
$mainFieldHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$mainFieldHtml[] = '<input type="hidden" name="' . $itemName . '" value="' . htmlspecialchars((string)$itemValue) . '" />';
$mainFieldHtml[] = '</typo3-backend-color-picker>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $this->data['databaseRow']['uid'] . '][' . $fieldName . ']');
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="form-check t3-form-field-eval-null-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = (string)($config['placeholder'] ?? '');
if ($placeholder !== '') {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="form-check t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$attributes = [
'recordFieldId' => $fieldId,
];
$resultArray['html'] = $renderedLabel . '
<typo3-formengine-element-color ' . GeneralUtility::implodeAttributes($attributes, true) . '>
<div class="formengine-field-item t3js-formengine-field-item">
' . $fieldInformationHtml . $fullElement . '
</div>
</typo3-formengine-element-color>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/color-element.js');
return $resultArray;
}
protected function resolveColorLabel(string $color, ?string $label): string
{
if ($label === null || $label === '') {
return $color;
}
$translatedLabel = $this->getLanguageService()->sL($label);
if ($translatedLabel === '') {
return $color;
}
return sprintf('%s (%s)', $translatedLabel, $color);
}
}
+267
View File
@@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Domain\DateTimeFormat;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of form elements with TCA type "datetime"
*/
class DatetimeElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
) {}
/**
* This will render a single-line datetime form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$format = $config['format'] ?? 'datetime';
if (!in_array($format, ['datetime', 'date', 'time', 'timesec', 'datetimesec'], true)) {
throw new \UnexpectedValueException(
'Format "' . $format . '" for field "' . $fieldName . '" in table "' . $table . '" is '
. 'not valid. Must be either empty or set to one of: "date", "datetime", "time", "timesec", "datetimesec".',
1647947686
);
}
$datetime = $parameterArray['itemFormElValue'];
if ($datetime !== null && !$datetime instanceof \DateTimeInterface) {
throw new \UnexpectedValueException(
'The formEngine itemFormElValue parameter for field "' . $fieldName . '" in table "' . $table . '" is '
. 'not valid. It must be an instance of `\\DateTimeInterface` but is `' . gettype($datetime) . '`. '
. 'Make sure to have it processed by `FormDataProvider/DatabaseRowDateTimeFields`.',
1731132127
);
}
$width = $this->formMaxWidth(MathUtility::forceIntegerInRange(
$config['size'] ?? ($format === 'datetimesec' ? 14 : ($format === 'date' || $format === 'datetime' ? 13 : 10)),
$this->minimumInputWidth,
$this->maxInputWidth
));
$fieldId = StringUtility::getUniqueId('formengine-input-');
$itemName = (string)$parameterArray['itemFormElName'];
$renderedLabel = $this->renderLabel($fieldId);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
if ($datetime === null) {
$itemValue = '';
} elseif ($format === 'time') {
$itemValue = (string)((int)$datetime->format('H') * 3600 + (int)$datetime->format('i') * 60);
} elseif ($format === 'timesec') {
$itemValue = (string)((int)$datetime->format('H') * 3600 + (int)$datetime->format('i') * 60 + (int)$datetime->format('s'));
} else {
$itemValue = (string)$datetime->getTimestamp();
}
// Format the unix-timestamp to the defined format (date/year etc)
$formattedDate = $this->formatValue($format, $itemValue);
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" name="' . htmlspecialchars($itemName) . '" value="' . htmlspecialchars($formattedDate) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$languageService = $this->getLanguageService();
// Always add the format to the eval list.
$evalList = [$format];
$isNullable = $config['nullable'] ?? false;
if ($isNullable) {
$evalList[] = 'null';
}
$attributes = [
'value' => '',
'id' => $fieldId,
'class' => implode(' ', [
'form-control',
'form-control-clearable',
]),
'data-input-type' => 'datetimepicker',
'data-date-type' => $format,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => (string)json_encode([
'field' => $itemName,
'evalList' => implode(',', $evalList),
], JSON_THROW_ON_ERROR),
'data-formengine-input-name' => $itemName,
];
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if ($format === 'datetime' || $format === 'date' || $format === 'datetimesec') {
if (isset($config['range']['lower'])) {
$lower = (int)$config['range']['lower'];
$attributes['data-date-min-date'] = date(DateTimeFormat::ISO8601_LOCALTIME, $lower);
}
if (isset($config['range']['upper'])) {
$upper = (int)$config['range']['upper'];
$attributes['data-date-max-date'] = date(DateTimeFormat::ISO8601_LOCALTIME, $upper);
}
}
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$buttonAriaLabelEscaped = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.datepicker.label'));
$dateISO8601 = $datetime?->format(DateTimeFormat::ISO8601_LOCALTIME) ?? '';
$expansionHtml = [];
$expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$expansionHtml[] = '<div class="form-wizards-wrap">';
$expansionHtml[] = '<div class="form-wizards-item-element">';
$expansionHtml[] = '<div class="input-group">';
$expansionHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$expansionHtml[] = '<input type="hidden" name="' . $itemName . '" value="' . htmlspecialchars($dateISO8601) . '" />';
$expansionHtml[] = '<button class="btn btn-default" aria-label="' . $buttonAriaLabelEscaped . '" type="button" data-global-event="click" data-action-focus="#' . $attributes['id'] . '">';
$expansionHtml[] = $this->iconFactory->getIcon('actions-edit-pick-date', IconSize::SMALL)->render();
$expansionHtml[] = '</button>';
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
if (!empty($fieldControlHtml)) {
$expansionHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$expansionHtml[] = '<div class="btn-group">';
$expansionHtml[] = $fieldControlHtml;
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$expansionHtml[] = '<div class="form-wizards-item-bottom">';
$expansionHtml[] = $fieldWizardHtml;
$expansionHtml[] = '</div>';
}
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
$expansionHtml = implode(LF, $expansionHtml);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $this->data['databaseRow']['uid'] . '][' . $fieldName . ']');
$fullElement = $expansionHtml;
if ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $datetime !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = (string)($config['placeholder'] ?? '');
if ($placeholder !== '') {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="form-check t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $expansionHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = $renderedLabel . '
<typo3-formengine-element-datetime class="formengine-field-item t3js-formengine-field-item" recordFieldId="' . htmlspecialchars($fieldId) . '">
' . $fieldInformationHtml . '
' . $fullElement . '
</typo3-formengine-element-datetime>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/datetime-element.js');
return $resultArray;
}
}
+238
View File
@@ -0,0 +1,238 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render elements of type email
*/
class EmailElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line email form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$itemValue = $parameterArray['itemFormElValue'];
$width = $this->formMaxWidth(
MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth)
);
$fieldId = StringUtility::getUniqueId('formengine-input-');
$itemName = (string)$parameterArray['itemFormElName'];
$renderedLabel = $this->renderLabel($fieldId);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" name="' . htmlspecialchars($itemName) . '" value="' . htmlspecialchars((string)$itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$languageService = $this->getLanguageService();
// Get filtered eval list, while always adding "trim"
$evalList = array_merge(array_filter(
GeneralUtility::trimExplode(',', $config['eval'] ?? '', true),
static fn(string $value): bool => in_array($value, ['unique', 'uniqueInPid'], true)
), ['trim']);
if ($config['nullable'] ?? false) {
$evalList[] = 'null';
}
$attributes = [
'value' => '',
'id' => $fieldId,
'maxlength' => '254',
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => (string)json_encode([
'field' => $itemName,
'evalList' => implode(',', $evalList),
], JSON_THROW_ON_ERROR),
'data-formengine-input-name' => $itemName,
];
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if (isset($config['autocomplete'])) {
$attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-item-element">';
if (is_array($config['valuePicker']['items'] ?? false)) {
$attributes['class'] = 'form-control';
$mainFieldHtml[] = '<typo3-backend-combobox>';
$mainFieldHtml[] = '<input type="email" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
foreach ($config['valuePicker']['items'] as $item) {
$mainFieldHtml[] = '<typo3-backend-combobox-choice value="' . htmlspecialchars($item['value']) . '">' . htmlspecialchars($languageService->sL($item['label'])) . '</typo3-backend-combobox-choice>';
}
$mainFieldHtml[] = '</typo3-backend-combobox>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/element/combobox-element.js');
} else {
$attributes['class'] = implode(' ', [
'form-control',
'form-control-clearable',
't3js-clearable',
]);
$mainFieldHtml[] = '<input type="email" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
}
$mainFieldHtml[] = '<input type="hidden" name="' . $itemName . '" value="' . htmlspecialchars((string)$itemValue) . '" />';
$mainFieldHtml[] = '</div>';
if (!empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $this->data['databaseRow']['uid'] . '][' . $fieldName . ']');
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="form-check t3-form-field-eval-null-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = trim((string)($config['placeholder'] ?? ''));
if ($placeholder !== '') {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="form-check t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = $renderedLabel . '
<div class="formengine-field-item t3js-formengine-field-item">
' . $fieldInformationHtml . $fullElement . '
</div>';
return $resultArray;
}
}
+97
View File
@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ProcessedFile;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* This renderType is used with type=user in FAL for table sys_file and
* sys_file_metadata, for field fileinfo and renders an informational
* element with image preview, filename, size and similar.
*/
class FileInfoElement extends AbstractFormElement
{
public function __construct(
private readonly ResourceFactory $resourceFactory,
) {}
/**
* Handler for single nodes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$fileUid = 0;
if ($this->data['tableName'] === 'sys_file') {
$fileUid = (int)$this->data['databaseRow']['uid'];
} elseif ($this->data['tableName'] === 'sys_file_metadata') {
$fileUid = (int)($this->data['databaseRow']['file'][0] ?? 0);
}
$fileObject = null;
if ($fileUid > 0) {
$fileObject = $this->resourceFactory->getFileObject($fileUid);
}
$resultArray['html'] = $this->renderFileInformationContent($fileObject);
return $resultArray;
}
/**
* Renders a HTML Block with file information
*/
protected function renderFileInformationContent(?File $file = null): string
{
$lang = $this->getLanguageService();
if ($file !== null) {
$content = '';
if ($file->isMissing()) {
$content .= '<span class="badge badge-danger badge-space-end">'
. htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
. '</span>';
}
if ($file->isImage() || $file->isMediaFile()) {
$processedFile = $file->process(ProcessedFile::CONTEXT_IMAGECROPSCALEMASK, ['width' => '150m', 'height' => '150m']);
$previewImage = $processedFile->getPublicUrl();
if ($previewImage) {
$content .= '<img src="' . htmlspecialchars($previewImage) . '" '
. 'width="' . $processedFile->getProperty('width') . '" '
. 'height="' . $processedFile->getProperty('height') . '" '
. 'alt="" class="t3-tceforms-sysfile-imagepreview" />';
}
}
$content .= '<strong>' . htmlspecialchars($file->getName()) . '</strong>';
$content .= ' (' . htmlspecialchars(GeneralUtility::formatSize((int)$file->getSize())) . 'bytes)<br />';
$content .= BackendUtility::getProcessedValue('sys_file', 'type', (string)$file->getType()) . ' (' . $file->getMimeType() . ')<br />';
$content .= htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:fileMetaDataLocation')) . ': ';
$content .= '<a href="' . htmlspecialchars($file->getPublicUrl() ?? '') . '" target="_blank" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view') . '">' . htmlspecialchars($file->getStorage()->getName()) . ' - ' . htmlspecialchars($file->getIdentifier()) . '</a><br />';
$content .= '<br />';
} else {
$content = '<h2>' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:fileMetaErrorInvalidRecord')) . '</h2>';
}
return $content;
}
}
+265
View File
@@ -0,0 +1,265 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of elements of the type "folder"
*/
class FolderElement extends AbstractFormElement
{
/**
* Default field controls for this element.
*
* @var array
*/
protected $defaultFieldControl = [
'elementBrowser' => [
'renderType' => 'elementBrowser',
],
];
/**
* Default field wizards for this element
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
'after' => [ 'recordsOverview' ],
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [ 'localizationStateSelector' ],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [ 'otherLanguageContent' ],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
) {}
/**
* This will render a selector box into which folder relations can be
* inserted.
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws \RuntimeException
*/
public function render(): array
{
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$elementName = $parameterArray['itemFormElName'];
$selectedItems = $parameterArray['itemFormElValue'];
$maxItems = $config['maxitems'];
$size = (int)($config['size'] ?? 5);
$autoSizeMax = (int)($config['autoSizeMax'] ?? 0);
if ($autoSizeMax > 0) {
$size = MathUtility::forceIntegerInRange($size, 1);
$size = MathUtility::forceIntegerInRange(count($selectedItems) + 1, $size, $autoSizeMax);
}
$fieldId = StringUtility::getUniqueId('tceforms-multiselect-');
$listOfSelectedValues = [];
$selectorOptionsHtml = [];
foreach ($selectedItems as $selectedItem) {
$folder = $selectedItem['folder'];
$listOfSelectedValues[] = $folder;
$selectorOptionsHtml[]
= '<option value="' . htmlspecialchars($folder) . '" title="' . htmlspecialchars($folder) . '">'
. htmlspecialchars($folder)
. '</option>';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if (isset($config['readOnly']) && $config['readOnly']) {
// Return early if element is read only
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<select';
$html[] = ' size="' . $size . '"';
$html[] = ' disabled="disabled"';
$html[] = ' id="' . $fieldId . '"';
$html[] = ' class="form-select"';
$html[] = ($maxItems !== 1 && $size !== 1) ? ' multiple="multiple"' : '';
$html[] = '>';
$html[] = implode(LF, $selectorOptionsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
$showMoveIcons = true;
if (isset($config['hideMoveIcons']) && $config['hideMoveIcons']) {
$showMoveIcons = false;
}
$showDeleteControl = true;
if (isset($config['hideDeleteIcon']) && $config['hideDeleteIcon']) {
$showDeleteControl = false;
}
$selectorAttributes = [
'id' => $fieldId,
'data-formengine-input-name' => htmlspecialchars($elementName),
'data-maxitems' => (string)$maxItems,
'size' => (string)$size,
];
$selectorAttributes['class'] = 'form-select';
if ($maxItems !== 1 && $size !== 1) {
$selectorAttributes['multiple'] = 'multiple';
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<input type="hidden" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . $itemCanBeSelectedMoreThanOnce . '" />';
$html[] = '<select ' . GeneralUtility::implodeAttributes($selectorAttributes, true) . '>';
$html[] = implode(LF, $selectorOptionsHtml);
$html[] = '</select>';
$html[] = '</div>';
if (($maxItems > 1 && $size > 1 && $showMoveIcons) || $showDeleteControl) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--move">';
$html[] = '<div class="btn-group-vertical">';
if ($maxItems > 1 && $size >= 2 && $showMoveIcons) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-top"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-top', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '</span>';
$html[] = '</button>';
}
if ($maxItems > 1 && $size > 1 && $showMoveIcons) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-up"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-up', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '</span>';
$html[] = '</button>';
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-down"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-down', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '</span>';
$html[] = '</button>';
}
if ($maxItems > 1 && $size >= 2 && $showMoveIcons) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-bottom"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-bottom', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '</span>';
$html[] = '</button>';
}
if ($showDeleteControl) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-removeoption t3js-revert-unique"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' data-uid="' . htmlspecialchars((string)$row['uid']) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-selection-delete', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '</span>';
$html[] = '</button>';
}
$html[] = '</div>';
$html[] = '</div>';
}
if ($fieldControlHtml !== '') {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = '<div class="btn-group-vertical">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$hiddenElementAttrs = array_merge(
[
'type' => 'hidden',
'name' => $elementName,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'value' => implode(',', $listOfSelectedValues),
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
$html[] = '<input ' . GeneralUtility::implodeAttributes($hiddenElementAttrs, true) . '>';
$html[] = '</div>';
$resultArray['html']
= '<typo3-formengine-element-folder class="formengine-field-item" recordFieldId="' . htmlspecialchars($fieldId) . '">
' . implode(LF, $html) . '
</typo3-formengine-element-folder>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/folder-element.js');
return $resultArray;
}
}
+370
View File
@@ -0,0 +1,370 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of elements of the type "group"
*/
class GroupElement extends AbstractFormElement
{
/**
* Default field controls for this element.
*
* @var array
*/
protected $defaultFieldControl = [
'elementBrowser' => [
'renderType' => 'elementBrowser',
],
'insertClipboard' => [
'renderType' => 'insertClipboard',
'after' => [ 'elementBrowser' ],
],
'editPopup' => [
'renderType' => 'editPopup',
'disabled' => true,
'after' => [ 'insertClipboard' ],
],
'addRecord' => [
'renderType' => 'addRecord',
'disabled' => true,
'after' => [ 'editPopup' ],
],
'listModule' => [
'renderType' => 'listModule',
'disabled' => true,
'after' => [ 'addRecord' ],
],
];
/**
* Default field wizards for this element
*
* @var array
*/
protected $defaultFieldWizard = [
'tableList' => [
'renderType' => 'tableList',
],
'recordsOverview' => [
'renderType' => 'recordsOverview',
'after' => [ 'tableList' ],
],
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
'after' => [ 'recordsOverview' ],
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [ 'localizationStateSelector' ],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [ 'otherLanguageContent' ],
],
'shortcutValidation' => [
'renderType' => 'shortcutValidation',
'after' => [ 'defaultLanguageDifferences' ],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
) {}
/**
* This will render a selector box into which elements from the database
* can be inserted. Relations.
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws \RuntimeException
*/
public function render(): array
{
$languageService = $this->getLanguageService();
$backendUser = $this->getBackendUserAuthentication();
$resultArray = $this->initializeResultArray();
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$elementName = $parameterArray['itemFormElName'];
$recordTypeValue = $this->data['recordTypeValue'] ?? null;
$selectedItems = $parameterArray['itemFormElValue'];
$maxItems = $config['maxitems'];
$size = (int)($config['size'] ?? 5);
$autoSizeMax = (int)($config['autoSizeMax'] ?? 0);
if ($autoSizeMax > 0) {
$size = MathUtility::forceIntegerInRange($size, 1);
$size = MathUtility::forceIntegerInRange(count($selectedItems) + 1, $size, $autoSizeMax);
}
$fieldId = StringUtility::getUniqueId('tceforms-multiselect-');
$listOfSelectedValues = [];
$selectorOptionsHtml = [];
foreach ($selectedItems as $selectedItem) {
$tableWithUid = $selectedItem['table'] . '_' . $selectedItem['uid'];
$listOfSelectedValues[] = $tableWithUid;
$title = $selectedItem['title'];
if (empty($title)) {
$title = '[' . $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']';
}
$shortenedTitle = BackendUtility::cropToTitleLength($title);
$selectorOptionsHtml[]
= '<option value="' . htmlspecialchars($tableWithUid) . '" title="' . htmlspecialchars($title) . '">'
. htmlspecialchars($this->appendValueToLabelInDebugMode($shortenedTitle, $tableWithUid))
. '</option>';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if (isset($config['readOnly']) && $config['readOnly']) {
// Return early if element is read only
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<select';
$html[] = ' size="' . $size . '"';
$html[] = ' disabled="disabled"';
$html[] = ' id="' . $fieldId . '"';
$html[] = ' class="form-select"';
$html[] = ($maxItems !== 1 && $size !== 1) ? ' multiple="multiple"' : '';
$html[] = '>';
$html[] = implode(LF, $selectorOptionsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
// Need some information if in flex form scope for the suggest element
$dataStructureIdentifier = '';
$flexFormSheetName = '';
$flexFormFieldName = '';
$flexFormContainerName = '';
$flexFormContainerFieldName = '';
if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
$flexFormConfig = $this->data['processedTca']['columns'][$fieldName];
$dataStructureIdentifier = $flexFormConfig['config']['dataStructureIdentifier'];
if (!isset($flexFormConfig['config']['dataStructureIdentifier'])) {
throw new \RuntimeException(
'A data structure identifier must be set in [\'config\'] part of a flex form.'
. ' This is usually added by TcaFlexPrepare data processor',
1485206970
);
}
if (isset($this->data['flexFormSheetName'])) {
$flexFormSheetName = $this->data['flexFormSheetName'];
}
if (isset($this->data['flexFormFieldName'])) {
$flexFormFieldName = $this->data['flexFormFieldName'];
}
if (isset($this->data['flexFormContainerName'])) {
$flexFormContainerName = $this->data['flexFormContainerName'];
}
if (isset($this->data['flexFormContainerFieldName'])) {
$flexFormContainerFieldName = $this->data['flexFormContainerFieldName'];
}
}
// Get minimum characters for suggest from TCA and override by TsConfig
$suggestMinimumCharacters = 0;
if (isset($config['suggestOptions']['default']['minimumCharacters'])) {
$suggestMinimumCharacters = (int)$config['suggestOptions']['default']['minimumCharacters'];
}
if (isset($parameterArray['fieldTSConfig']['suggest.']['default.']['minimumCharacters'])) {
$suggestMinimumCharacters = (int)$parameterArray['fieldTSConfig']['suggest.']['default.']['minimumCharacters'];
}
$suggestMinimumCharacters = $suggestMinimumCharacters > 0 ? $suggestMinimumCharacters : 2;
$itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
$showMoveIcons = true;
if (isset($config['hideMoveIcons']) && $config['hideMoveIcons']) {
$showMoveIcons = false;
}
$showDeleteControl = true;
if (isset($config['hideDeleteIcon']) && $config['hideDeleteIcon']) {
$showDeleteControl = false;
}
$selectorAttributes = [
'id' => $fieldId,
'data-formengine-input-name' => htmlspecialchars($elementName),
'data-maxitems' => (string)$maxItems,
'size' => (string)$size,
];
$selectorAttributes['class'] = 'form-select';
if ($maxItems !== 1 && $size !== 1) {
$selectorAttributes['multiple'] = 'multiple';
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
if (!isset($config['hideSuggest']) || (bool)$config['hideSuggest'] !== true) {
$html[] = '<div class="form-wizards-item-top">';
$html[] = '<div class="autocomplete t3-form-suggest-container">';
$html[] = '<input type="search" autocomplete="off" class="t3-form-suggest form-control"';
$html[] = ' placeholder="' . $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_alt_doc.xlf:search.find_record') . '"';
$html[] = ' data-fieldname="' . htmlspecialchars($fieldName) . '"';
$html[] = ' data-tablename="' . htmlspecialchars($table) . '"';
$html[] = ' data-field="' . htmlspecialchars($elementName) . '"';
$html[] = ' data-uid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"';
$html[] = ' data-pid="' . htmlspecialchars($this->data['parentPageRow']['uid'] ?? 0) . '"';
$html[] = ' data-fieldtype="' . htmlspecialchars($config['type']) . '"';
$html[] = ' data-minchars="' . htmlspecialchars((string)$suggestMinimumCharacters) . '"';
$html[] = ' data-datastructureidentifier="' . htmlspecialchars($dataStructureIdentifier) . '"';
$html[] = ' data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
$html[] = ' data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
$html[] = ' data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
$html[] = ' data-flexformcontainerfieldname="' . htmlspecialchars($flexFormContainerFieldName) . '"';
if ($recordTypeValue !== null && $recordTypeValue !== '') {
$html[] = ' data-recordtypevalue="' . htmlspecialchars($recordTypeValue) . '"';
}
$html[] = '/>';
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<input type="hidden" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . $itemCanBeSelectedMoreThanOnce . '" />';
$html[] = '<select ' . GeneralUtility::implodeAttributes($selectorAttributes, true) . '>';
$html[] = implode(LF, $selectorOptionsHtml);
$html[] = '</select>';
$html[] = '</div>';
if (($maxItems > 1 && $size > 1 && $showMoveIcons) || $showDeleteControl) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--move">';
$html[] = '<div class="btn-group-vertical">';
if ($maxItems > 1 && $size >= 2 && $showMoveIcons) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-top"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-top', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '</span>';
$html[] = '</button>';
}
if ($maxItems > 1 && $size > 1 && $showMoveIcons) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-up"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-up', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '</span>';
$html[] = '</button>';
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-down"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-down', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '</span>';
$html[] = '</button>';
}
if ($maxItems > 1 && $size >= 2 && $showMoveIcons) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-bottom"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-bottom', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '</span>';
$html[] = '</button>';
}
if ($showDeleteControl) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-removeoption t3js-revert-unique"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' data-uid="' . htmlspecialchars($row['uid']) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-selection-delete', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '</span>';
$html[] = '</button>';
}
$html[] = '</div>';
$html[] = '</div>';
}
if ($fieldControlHtml !== '') {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = '<div class="btn-group-vertical">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$hiddenElementAttrs = array_merge(
[
'type' => 'hidden',
'name' => $elementName,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'value' => implode(',', $listOfSelectedValues),
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
$html[] = '<input ' . GeneralUtility::implodeAttributes($hiddenElementAttrs, true) . '>';
$html[] = '</div>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/group-element.js'
)->instance($fieldId);
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
protected function getBackendUserAuthentication(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,334 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Form\Event\ModifyImageManipulationPreviewUrlEvent;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Backend\View\BackendViewFactory;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Imaging\ImageManipulation\Area;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
use TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException;
use TYPO3\CMS\Core\Imaging\ImageManipulation\Ratio;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Generation of image manipulation FormEngine element.
* This is typically used in FAL relations to cut images.
*/
class ImageManipulationElement extends AbstractFormElement
{
private string $wizardRouteName = 'ajax_wizard_image_manipulation';
/**
* Default element configuration
*
* @var array
*/
protected static $defaultConfig = [
'file_field' => 'uid_local',
'allowedExtensions' => null, // default: $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
'cropVariants' => [
'default' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.crop_variant.default',
'allowedAspectRatios' => [
'16:9' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.16_9',
'value' => 16 / 9,
],
'3:2' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.3_2',
'value' => 3 / 2,
],
'4:3' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.4_3',
'value' => 4 / 3,
],
'1:1' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.1_1',
'value' => 1.0,
],
'NaN' => [
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:imwizard.ratio.free',
'value' => 0.0,
],
],
'excludeFromSync' => false,
'selectedRatio' => 'NaN',
'cropArea' => [
'x' => 0.0,
'y' => 0.0,
'width' => 1.0,
'height' => 1.0,
],
],
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageThumbnails' => [
'renderType' => 'otherLanguageThumbnails',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageThumbnails',
],
],
];
public function __construct(
private readonly BackendViewFactory $backendViewFactory,
private readonly UriBuilder $uriBuilder,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly ResourceFactory $resourceFactory,
private readonly HashService $hashService,
) {}
/**
* This will render an imageManipulation field
*
* @return array As defined in initializeResultArray() of AbstractNode
* @throws InvalidConfigurationException
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $this->populateConfiguration($parameterArray['fieldConf']['config']);
$file = $this->getFile($this->data['databaseRow'], $config['file_field']);
if (!$file) {
// Early return in case we do not find a file
return $resultArray;
}
$config = $this->processConfiguration($config, $parameterArray['itemFormElValue'], $file);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$arguments = [
'fieldInformation' => $fieldInformationHtml,
'fieldControl' => $fieldControlHtml,
'fieldWizard' => $fieldWizardHtml,
'isAllowedFileExtension' => in_array(strtolower($file->getExtension()), GeneralUtility::trimExplode(',', strtolower($config['allowedExtensions'])), true),
'image' => $file,
'formEngine' => [
'field' => [
'value' => $parameterArray['itemFormElValue'],
'name' => $parameterArray['itemFormElName'],
],
'validation' => '[]',
],
'config' => $config,
'wizardUri' => $this->getWizardUri(),
'wizardPayload' => json_encode($this->getWizardPayload($config['cropVariants'], $file)),
'previewUrl' => $this->eventDispatcher->dispatch(
new ModifyImageManipulationPreviewUrlEvent($this->data['databaseRow'], $config, $file)
)->getPreviewUrl(),
];
if ($arguments['isAllowedFileExtension']) {
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/image-manipulation.js'
)->invoke('initializeTrigger');
$arguments['formEngine']['field']['id'] = StringUtility::getUniqueId('formengine-image-manipulation-');
if ($config['required'] ?? false) {
$arguments['formEngine']['validation'] = $this->getValidationDataAsJsonString(['required' => true]);
}
}
$view = $this->backendViewFactory->create($this->data['request']);
$view->assignMultiple($arguments);
$resultArray['html'] = $this->wrapWithFieldsetAndLegend($view->render('Form/ImageManipulationElement'));
return $resultArray;
}
/**
* Get file object
*
* @param string $fieldName
* @return File|null
*/
protected function getFile(array $row, $fieldName)
{
$file = null;
$fileUid = !empty($row[$fieldName]) ? $row[$fieldName] : null;
if (is_array($fileUid) && isset($fileUid[0]['uid'])) {
$fileUid = $fileUid[0]['uid'];
}
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
try {
$file = $this->resourceFactory->getFileObject($fileUid);
} catch (FileDoesNotExistException|\InvalidArgumentException) {
}
}
return $file;
}
/**
* @return array
* @throws InvalidConfigurationException
*/
protected function populateConfiguration(array $baseConfiguration)
{
$defaultConfig = self::$defaultConfig;
// If ratios are set do not add default options
if (isset($baseConfiguration['cropVariants'])) {
unset($defaultConfig['cropVariants']);
}
$config = array_replace_recursive($defaultConfig, $baseConfiguration);
if (!is_array($config['cropVariants'])) {
throw new InvalidConfigurationException('Crop variants configuration must be an array', 1485377267);
}
$cropVariants = [];
foreach ($config['cropVariants'] as $id => $cropVariant) {
// Filter allowed aspect ratios
$cropVariant['allowedAspectRatios'] = array_filter($cropVariant['allowedAspectRatios'] ?? [], static function (array $aspectRatio): bool {
return !(bool)($aspectRatio['disabled'] ?? false);
});
// Aspect ratios may not contain a "." character, see Ratio::__construct()
// To match them again properly, same replacement is required here.
$preparedAllowedAspectRatios = [];
foreach ($cropVariant['allowedAspectRatios'] as $aspectRatio => $aspectRatioDefinition) {
$preparedAllowedAspectRatios[Ratio::prepareAspectRatioId($aspectRatio)] = $aspectRatioDefinition;
}
$cropVariant['allowedAspectRatios'] = $preparedAllowedAspectRatios;
// Ignore disabled crop variants
if (!empty($cropVariant['disabled'])) {
continue;
}
if (empty($cropVariant['allowedAspectRatios'])) {
throw new InvalidConfigurationException('Crop variants configuration ' . $id . ' contains no allowed aspect ratios', 1620147893);
}
// Enforce a crop area (default is full image)
if (empty($cropVariant['cropArea'])) {
$cropVariant['cropArea'] = Area::createEmpty()->asArray();
}
$cropVariants[$id] = $cropVariant;
}
$config['cropVariants'] = $cropVariants;
// By default we allow all image extensions that can be handled by the GFX functionality
$config['allowedExtensions'] ??= $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'];
return $config;
}
/**
* @return array
* @throws InvalidConfigurationException
*/
protected function processConfiguration(array $config, string &$elementValue, File $file)
{
$cropVariantsAreEqual = $this->checkIfCropVariantsAreEqual($config['cropVariants']);
$cropVariantCollection = CropVariantCollection::create($elementValue, $config['cropVariants']);
if (empty($config['readOnly']) && !empty($file->getProperty('width'))) {
$cropVariantCollection = $cropVariantCollection->applyRatioRestrictionToSelectedCropArea($file);
$elementValue = (string)$cropVariantCollection;
}
$config['cropVariants'] = $cropVariantCollection->asArray();
$config['allowedExtensions'] = implode(', ', GeneralUtility::trimExplode(',', $config['allowedExtensions'], true));
$config['syncAvailable'] = $cropVariantsAreEqual;
return $config;
}
protected function getWizardUri(): string
{
return (string)$this->uriBuilder->buildUriFromRoute($this->wizardRouteName);
}
protected function getWizardPayload(array $cropVariants, File $image): array
{
$uriArguments = [];
$arguments = [
'cropVariants' => $cropVariants,
'image' => $image->getUid(),
];
$uriArguments['arguments'] = json_encode($arguments);
$uriArguments['signature'] = $this->hashService->hmac((string)$uriArguments['arguments'], $this->wizardRouteName);
return $uriArguments;
}
protected function checkIfCropVariantsAreEqual(array $cropVariants): bool
{
$validVariants = array_filter(
$cropVariants,
static fn($variant) => ! ($variant['excludeFromSync'] ?? false)
);
if (count($validVariants) <= 1) {
return true;
}
$processedVariants = array_map(static function (array $variant) {
// Remove the title for comparison, because it must be different and does not disturb the feature
unset($variant['title']);
$variant = ArrayUtility::sortByKeyRecursive($variant);
return ArrayUtility::flatten($variant);
}, $validVariants);
$first = array_shift($processedVariants);
return array_reduce(
$processedVariants,
static fn(bool $allEqualSoFar, array $currentVariant) => $allEqualSoFar && ($currentVariant === $first),
true
);
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
/**
* Generation of TCEform elements of the type "input type=hidden"
*/
class InputHiddenElement extends AbstractFormElement
{
/**
* This will render an input type="hidden" form field
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$resultArray['html'] .= '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
return $resultArray;
}
}
+264
View File
@@ -0,0 +1,264 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Controller\FormSlugAjaxController;
use TYPO3\CMS\Core\Crypto\HashService;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* General type=input element for TCA Type=Slug with some additional value.
*/
class InputSlugElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
private readonly HashService $hashService,
) {}
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$languageId = 0;
if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) {
$languageField = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
$languageId = (int)((is_array($row[$languageField] ?? null) ? ($row[$languageField][0] ?? 0) : $row[$languageField]) ?? 0);
}
$itemValue = $parameterArray['itemFormElValue'];
$config = $parameterArray['fieldConf']['config'];
$evalList = GeneralUtility::trimExplode(',', $config['eval'] ?? '', true);
$size = MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth);
$width = $this->formMaxWidth($size);
$baseUrl = $this->data['customData'][$this->data['fieldName']]['slugPrefix'] ?? '';
$fieldId = StringUtility::getUniqueId('formengine-input-');
$renderedLabel = $this->renderLabel($fieldId);
// Convert UTF-8 characters back (that is important, see Slug class when sanitizing)
$itemValue = rawurldecode($itemValue);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
// readOnly is not supported as columns config but might be set by SingleFieldContainer in case
// "l10n_display" is set to "defaultAsReadonly". To prevent misbehaviour for fields, which falsely
// set this, we also check for "defaultAsReadonly" being set and whether the record is an overlay.
if (($config['readOnly'] ?? false)
&& ($this->data['processedTca']['ctrl']['transOrigPointerField'] ?? false)
&& ($row[$this->data['processedTca']['ctrl']['transOrigPointerField']][0] ?? $row[$this->data['processedTca']['ctrl']['transOrigPointerField']] ?? false)
&& GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'] ?? '', 'defaultAsReadonly')
) {
$disabledFieldAttributes = [
'class' => 'form-control',
'data-formengine-input-name' => $parameterArray['itemFormElName'],
'type' => 'text',
'value' => $itemValue,
'title' => $itemValue,
'id' => $fieldId,
];
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="input-group">';
$html[] = ($baseUrl ? '<span class="input-group-text">' . htmlspecialchars($baseUrl) . '</span>' : '');
$html[] = '<input ' . GeneralUtility::implodeAttributes($disabledFieldAttributes, true) . ' disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$toggleButtonTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.toggleSlugExplanation');
$recreateButtonTitle = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.recreateSlugExplanation');
$successMessage = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:slugCreation.success.' . ($table === 'pages' ? 'page' : 'record'));
$errorMessage = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:slugCreation.error');
$thisSlugId = 't3js-form-field-slug-id' . StringUtility::getUniqueId();
$mainFieldHtml = [];
$mainFieldHtml[] = $renderedLabel;
$mainFieldHtml[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$mainFieldHtml[] = $fieldInformationHtml;
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px" id="' . htmlspecialchars($thisSlugId) . '">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-item-element">';
$mainFieldHtml[] = '<div class="input-group">';
$mainFieldHtml[] = ($baseUrl ? '<span class="input-group-text">' . htmlspecialchars($baseUrl) . '</span>' : '');
// We deal with 3 fields here: a readonly field for current / default values, an input
// field to manipulate the value, and the final hidden field used to send the value
$mainFieldHtml[] = '<input';
$mainFieldHtml[] = ' class="form-control t3js-form-field-slug-readonly"';
$mainFieldHtml[] = ' title="' . htmlspecialchars($itemValue) . '"';
$mainFieldHtml[] = ' value="' . htmlspecialchars($itemValue) . '"';
$mainFieldHtml[] = ' readonly';
$mainFieldHtml[] = ' />';
$mainFieldHtml[] = '<input type="text"';
$mainFieldHtml[] = ' id="' . htmlspecialchars($fieldId) . '"';
$mainFieldHtml[] = ' class="form-control t3js-form-field-slug-input hidden"';
$mainFieldHtml[] = ' placeholder="' . htmlspecialchars($row['slug'] ?? '/') . '"';
$mainFieldHtml[] = ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
$mainFieldHtml[] = ' data-formengine-input-params="' . htmlspecialchars((string)json_encode(['field' => $parameterArray['itemFormElName'], 'evalList' => implode(',', $evalList)])) . '"';
$mainFieldHtml[] = ' data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$mainFieldHtml[] = ' />';
$mainFieldHtml[] = '<input type="hidden"';
$mainFieldHtml[] = ' class="t3js-form-field-slug-hidden"';
$mainFieldHtml[] = ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$mainFieldHtml[] = ' value="' . htmlspecialchars($itemValue) . '"';
$mainFieldHtml[] = ' />';
$mainFieldHtml[] = '<button class="btn btn-default t3js-form-field-slug-toggle" type="button" title="' . htmlspecialchars($toggleButtonTitle) . '">';
$mainFieldHtml[] = $this->iconFactory->getIcon('actions-version-workspaces-preview-link', IconSize::SMALL)->render();
$mainFieldHtml[] = '</button>';
$mainFieldHtml[] = '<button class="btn btn-default t3js-form-field-slug-recreate" type="button" title="' . htmlspecialchars($recreateButtonTitle) . '">';
$mainFieldHtml[] = $this->iconFactory->getIcon('actions-refresh', IconSize::SMALL)->render();
$mainFieldHtml[] = '</button>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
if (!empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '<div class="form-wizards-item-bottom">';
$mainFieldHtml[] = '<div class="t3js-form-proposal-accepted callout callout-success hidden mt-3 mb-0">';
$mainFieldHtml[] = '<div class="callout-content">';
$mainFieldHtml[] = '<div class="callout-body">';
$mainFieldHtml[] = sprintf(htmlspecialchars($successMessage), '<samp class="text-nowrap">' . htmlspecialchars($baseUrl) . '<span class="fw-bold">/abc/</span></samp>');
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '<div class="t3js-form-proposal-different callout callout-warning hidden mt-3 mb-0">';
$mainFieldHtml[] = '<div class="callout-content">';
$mainFieldHtml[] = '<div class="callout-body">';
$mainFieldHtml[] = sprintf(htmlspecialchars($errorMessage), '<samp class="text-nowrap">' . htmlspecialchars($baseUrl) . '<span class="fw-bold">/abc/</span></samp>');
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$resultArray['html'] = implode(LF, $mainFieldHtml);
[$commonElementPrefix] = GeneralUtility::revExplode('[', $parameterArray['itemFormElName'], 2);
$validInputNamesToListenTo = [];
$includeUidInValues = false;
foreach ($config['generatorOptions']['fields'] ?? [] as $fieldNameParts) {
if (is_string($fieldNameParts)) {
$fieldNameParts = GeneralUtility::trimExplode(',', $fieldNameParts);
}
foreach ($fieldNameParts as $listenerFieldName) {
if ($listenerFieldName === 'uid') {
$includeUidInValues = true;
continue;
}
$validInputNamesToListenTo[$listenerFieldName] = $commonElementPrefix . '[' . htmlspecialchars($listenerFieldName) . ']';
}
}
$parentPageId = $this->data['parentPageRow']['uid'] ?? 0;
$signature = $this->hashService->hmac(
implode(
'',
[
$table,
$this->data['effectivePid'],
$row['uid'],
$languageId,
$this->data['fieldName'],
$this->data['command'],
$parentPageId,
]
),
FormSlugAjaxController::class
);
$optionsForModule = [
'pageId' => $this->data['effectivePid'],
'recordId' => $row['uid'],
'tableName' => $table,
'fieldName' => $this->data['fieldName'],
'config' => $config,
'listenerFieldNames' => $validInputNamesToListenTo,
'language' => $languageId,
'originalValue' => $itemValue,
'signature' => $signature,
'command' => $this->data['command'],
'parentPageId' => $parentPageId,
'includeUidInValues' => $includeUidInValues,
];
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/slug-element.js'
)->instance('#' . $thisSlugId, $optionsForModule);
return $resultArray;
}
}
+276
View File
@@ -0,0 +1,276 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* General type=input element
*
* The InputTextElement renders a html input field with the type "text" attribute.
*/
class InputTextElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$itemValue = $parameterArray['itemFormElValue'];
$width = $this->formMaxWidth(
MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth)
);
$fieldId = StringUtility::getUniqueId('formengine-input-');
$itemName = (string)$parameterArray['itemFormElName'];
$renderedLabel = $this->renderLabel($fieldId);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" name="' . htmlspecialchars($itemName) . '" value="' . htmlspecialchars((string)$itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$languageService = $this->getLanguageService();
// @todo: The whole eval handling is a mess and needs refactoring
$evalList = GeneralUtility::trimExplode(',', $config['eval'] ?? '', true);
foreach ($evalList as $func) {
// @todo: This is ugly: The code should find out on it's own whether an eval definition is a
// @todo: keyword like "date", or a class reference. The global registration could be dropped then
// Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
if (class_exists($func)) {
$evalObj = GeneralUtility::makeInstance($func);
if (method_exists($evalObj, 'deevaluateFieldValue')) {
$_params = [
'value' => $itemValue,
];
$itemValue = $evalObj->deevaluateFieldValue($_params);
}
$resultArray = $this->resolveJavaScriptEvaluation($resultArray, $func, $evalObj);
}
}
}
if ($config['nullable'] ?? false) {
$evalList[] = 'null';
}
$formEngineInputParams = [
'field' => $itemName,
];
// The `is_in` constraint requires two parameters to work: the "eval" setting and a configuration of the
// actually allowed characters
if (in_array('is_in', $evalList, true)) {
if (($config['is_in'] ?? '') !== '') {
$formEngineInputParams['is_in'] = $config['is_in'];
} else {
$evalList = array_diff($evalList, ['is_in']);
}
} else {
unset($config['is_in']);
}
if ($evalList !== []) {
$formEngineInputParams['evalList'] = implode(',', $evalList);
}
$attributes = [
'value' => '',
'id' => $fieldId,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => (string)json_encode($formEngineInputParams, JSON_THROW_ON_ERROR),
'data-formengine-input-name' => $itemName,
];
$maxLength = (int)($config['max'] ?? 0);
if ($maxLength > 0) {
$attributes['maxlength'] = (string)$maxLength;
}
$minLength = (int)($config['min'] ?? 0);
if ($minLength > 0 && ($maxLength === 0 || $minLength <= $maxLength)) {
$attributes['minlength'] = (string)$minLength;
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if (isset($config['autocomplete'])) {
$attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-item-element">';
if (is_array($config['valuePicker']['items'] ?? false)) {
$attributes['class'] = 'form-control';
$mainFieldHtml[] = '<typo3-backend-combobox>';
$mainFieldHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
foreach ($config['valuePicker']['items'] as $item) {
$mainFieldHtml[] = '<typo3-backend-combobox-choice value="' . htmlspecialchars($item['value']) . '">' . htmlspecialchars($languageService->sL($item['label'])) . '</typo3-backend-combobox-choice>';
}
$mainFieldHtml[] = '</typo3-backend-combobox>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/element/combobox-element.js');
} else {
$attributes['class'] = implode(' ', [
'form-control',
'form-control-clearable',
't3js-clearable',
]);
$mainFieldHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
}
$mainFieldHtml[] = '<input type="hidden" name="' . $itemName . '" value="' . htmlspecialchars((string)$itemValue) . '" />';
$mainFieldHtml[] = '</div>';
if (!empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $this->data['databaseRow']['uid'] . '][' . $fieldName . ']');
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="form-check t3-form-field-eval-null-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = trim((string)($config['placeholder'] ?? ''));
if ($placeholder !== '') {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="form-check t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = $renderedLabel . '
<div class="formengine-field-item t3js-formengine-field-item">
' . $fieldInformationHtml . $fullElement . '
</div>';
return $resultArray;
}
}
+234
View File
@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\CodeEditor\CodeEditor;
use TYPO3\CMS\Backend\CodeEditor\Registry\AddonRegistry;
use TYPO3\CMS\Backend\CodeEditor\Registry\ModeRegistry;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Handles type=json elements.
*
* Renders either a code editor or a standard textarea.
*/
class JsonElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
public function render(): array
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$readOnly = (bool)($config['readOnly'] ?? false);
$placeholder = trim((string)($config['placeholder'] ?? ''));
$enableCodeEditor = $config['enableCodeEditor'] ?? true;
$itemValue = '';
if (!empty($parameterArray['itemFormElValue'])) {
try {
$itemValue = (string)json_encode($parameterArray['itemFormElValue'], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
} catch (\JsonException) {
}
}
$width = null;
if ($config['cols'] ?? false) {
$width = $this->formMaxWidth(MathUtility::forceIntegerInRange($config['cols'], $this->minimumInputWidth, $this->maxInputWidth));
}
$rows = MathUtility::forceIntegerInRange(($config['rows'] ?? 5) ?: 5, 1, 20);
$originalRows = $rows;
if (($itemFormElementValueLength = strlen($itemValue)) > 80) {
$calculatedRows = MathUtility::forceIntegerInRange(
(int)round($itemFormElementValueLength / 40),
count(explode(LF, $itemValue)),
20
);
if ($originalRows < $calculatedRows) {
$rows = $calculatedRows;
}
}
$fieldId = StringUtility::getUniqueId('formengine-json-');
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($readOnly && !$enableCodeEditor) {
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px">' : '>');
$html[] = '<textarea class="form-control font-monospace" id="' . htmlspecialchars($fieldId) . '" rows="' . $rows . '" disabled>';
$html[] = htmlspecialchars($itemValue);
$html[] = '</textarea>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$itemName = (string)$parameterArray['itemFormElName'];
$attributes = [
'id' => $fieldId,
'name' => $itemName,
'wrap' => 'off',
'rows' => (string)$rows,
'class' => 'form-control font-monospace',
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
];
if ($readOnly) {
$attributes['disabled'] = '';
}
if ($placeholder !== '') {
$attributes['placeholder'] = $placeholder;
}
// Use CodeMirror if available
if ($enableCodeEditor) {
// Compile and register code editor configuration
GeneralUtility::makeInstance(CodeEditor::class)->registerConfiguration();
$modeRegistry = GeneralUtility::makeInstance(ModeRegistry::class);
$mode = $modeRegistry->isRegistered('json')
? $modeRegistry->getByFormatCode('json')
: $modeRegistry->getDefaultMode();
$addons = $keymaps = [];
foreach (GeneralUtility::makeInstance(AddonRegistry::class)->getAddons() as $addon) {
foreach ($addon->getCssFiles() as $cssFile) {
$resultArray['stylesheetFiles'][] = $cssFile;
}
if (($module = $addon->getModule())) {
$addons[] = $module;
}
if (($keymap = $addon->getKeymap())) {
$keymaps[] = $keymap;
}
}
$codeMirrorConfig = [
'mode' => GeneralUtility::jsonEncodeForHtmlAttribute($mode->getModule(), false),
];
if ($readOnly) {
$codeMirrorConfig['readonly'] = '';
}
if ($placeholder !== '') {
$codeMirrorConfig['placeholder'] = $placeholder;
}
if ($addons !== []) {
$codeMirrorConfig['addons'] = GeneralUtility::jsonEncodeForHtmlAttribute($addons, false);
}
if ($keymaps !== []) {
$codeMirrorConfig['keymaps'] = GeneralUtility::jsonEncodeForHtmlAttribute($keymaps, false);
}
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/code-editor/element/code-mirror-element.js');
$editorHtml = '
<typo3-t3editor-codemirror ' . GeneralUtility::implodeAttributes($codeMirrorConfig, true, true) . '>
<textarea ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>' . htmlspecialchars($itemValue) . '</textarea>
<input type="hidden" name="target" value="0" />
<input type="hidden" name="effectivePid" value="' . htmlspecialchars((string)($this->data['effectivePid'] ?? '0')) . '" />
</typo3-t3editor-codemirror>';
} else {
$attributes['class'] = implode(' ', array_merge(explode(' ', $attributes['class']), ['formengine-textarea', 't3js-enable-tab']));
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/json-element.js');
$editorHtml = '
<typo3-formengine-element-json recordFieldId="' . htmlspecialchars($fieldId) . '">
<textarea ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>' . htmlspecialchars($itemValue) . '</textarea>
</typo3-formengine-element-json>';
}
$additionalHtml = [];
if (!$readOnly) {
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
if (!empty($fieldControlHtml)) {
$additionalHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$additionalHtml[] = '<div class="btn-group">';
$additionalHtml[] = $fieldControlHtml;
$additionalHtml[] = '</div>';
$additionalHtml[] = '</div>';
}
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
if (!empty($fieldWizardHtml)) {
$additionalHtml[] = '<div class="form-wizards-item-bottom">';
$additionalHtml[] = $fieldWizardHtml;
$additionalHtml[] = '</div>';
}
}
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px">' : '>');
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = $editorHtml;
$html[] = '</div>';
$html[] = implode(LF, $additionalHtml);
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
}
+521
View File
@@ -0,0 +1,521 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use Psr\EventDispatcher\EventDispatcherInterface;
use TYPO3\CMS\Backend\Form\Event\ModifyLinkExplanationEvent;
use TYPO3\CMS\Backend\LinkHandler\RecordLinkHandler;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\LinkHandling\Exception\UnknownLinkHandlerException;
use TYPO3\CMS\Core\LinkHandling\LinkService;
use TYPO3\CMS\Core\LinkHandling\TypoLinkCodecService;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
use TYPO3\CMS\Core\Resource\File;
use TYPO3\CMS\Core\Resource\Folder;
use TYPO3\CMS\Core\Type\Bitmask\Permission;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Link element.
*
* Shows current link and the link popup.
*/
class LinkElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly TypoLinkCodecService $typoLinkCodecService,
private readonly LinkService $linkService,
) {}
/**
* This will render a single-line link form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
if (is_array($config['allowedTypes'] ?? false) && $config['allowedTypes'] === []) {
throw new \RuntimeException(
'Field "' . $fieldName . '" in table "' . $table . '" of type "link" defines an empty list of allowed link types.',
1646922484
);
}
$itemValue = $parameterArray['itemFormElValue'];
$width = $this->formMaxWidth(
MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth)
);
$fieldId = StringUtility::getUniqueId('formengine-input-');
$itemName = (string)$parameterArray['itemFormElName'];
$renderedLabel = $this->renderLabel($fieldId);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" name="' . htmlspecialchars($itemName) . '" value="' . htmlspecialchars((string)$itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$languageService = $this->getLanguageService();
// Always adding "trim".
$evalList = ['trim'];
if ($config['nullable'] ?? false) {
$evalList[] = 'null';
}
$attributes = [
'value' => '',
'id' => $fieldId,
'class' => implode(' ', [
'form-control',
'form-control-clearable',
't3js-clearable',
't3js-form-field-link-input',
]),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => (string)json_encode([
'field' => $itemName,
'evalList' => implode(',', $evalList),
], JSON_THROW_ON_ERROR),
'data-formengine-input-name' => $itemName,
];
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if (isset($config['autocomplete'])) {
$attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
}
$valuePickerHtml = [];
if (is_array($config['valuePicker']['items'] ?? false)) {
$valuePickerConfiguration = [
'linked-field' => '[data-formengine-input-name="' . $itemName . '"]',
'class' => 'form-control-clearable-wrapper',
];
$valuePickerAttributes = array_merge(
[
'class' => 'form-select form-control-adapt',
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
$valuePickerHtml[] = '<typo3-formengine-valuepicker ' . GeneralUtility::implodeAttributes($valuePickerConfiguration, true) . '>';
$valuePickerHtml[] = '<select ' . GeneralUtility::implodeAttributes($valuePickerAttributes, true) . '>';
$valuePickerHtml[] = '<option></option>';
foreach ($config['valuePicker']['items'] as $item) {
$valuePickerHtml[] = '<option value="' . htmlspecialchars($item['value']) . '">' . htmlspecialchars($languageService->sL($item['label'])) . '</option>';
}
$valuePickerHtml[] = '</select>';
$valuePickerHtml[] = '</typo3-formengine-valuepicker>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-wizard/value-picker.js');
}
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
// Manually initialize the "linkPopup" FieldControl configuration, based on the link type specific settings
$this->initializeLinkPopup($config);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$linkExplanation = $this->getLinkExplanation((string)$itemValue);
$hasExplanation = ($linkExplanation['text'] ?? '') !== '';
if ($hasExplanation) {
$attributes['hidden'] = 'hidden';
}
$explanation = htmlspecialchars($linkExplanation['text'] ?? '');
$toggleButtonTitle = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.toggleLinkExplanation');
$expansionHtml = [];
$expansionHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$expansionHtml[] = '<div class="form-wizards-wrap">';
$expansionHtml[] = '<div class="form-wizards-item-element">';
$expansionHtml[] = '<div class="input-group t3js-form-field-link">';
$expansionHtml[] = '<span class="t3js-form-field-link-icon input-group-text">' . ($linkExplanation['icon'] ?? '') . '</span>';
$expansionHtml[] = '<input class="form-control form-control-explanation t3js-form-field-link-explanation" title="' . $explanation . '" value="' . $explanation . '"' . ' readonly' . ($hasExplanation ? '' : ' hidden') . '>';
$expansionHtml[] = '<input type="text" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$expansionHtml[] = '<input type="hidden" name="' . $itemName . '" value="' . htmlspecialchars((string)$itemValue) . '" />';
$expansionHtml[] = '<button class="btn btn-default t3js-form-field-link-explanation-toggle" type="button" title="' . htmlspecialchars($toggleButtonTitle) . '"' . ($hasExplanation ? '' : ' disabled') . '>';
$expansionHtml[] = $this->iconFactory->getIcon('actions-version-workspaces-preview-link', IconSize::SMALL)->render();
$expansionHtml[] = '</button>';
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
if (!empty($valuePickerHtml) || !empty($fieldControlHtml)) {
$expansionHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$expansionHtml[] = '<div class="input-group">';
$expansionHtml[] = implode(LF, $valuePickerHtml);
$expansionHtml[] = $fieldControlHtml;
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
}
if (!empty($fieldWizardHtml) || !empty($linkExplanation['additionalAttributes'])) {
$expansionHtml[] = '<div class="form-wizards-item-bottom">';
$expansionHtml[] = $linkExplanation['additionalAttributes'] ?? '';
$expansionHtml[] = $fieldWizardHtml;
$expansionHtml[] = '</div>';
}
$expansionHtml[] = '</div>';
$expansionHtml[] = '</div>';
$expansionHtml = implode(LF, $expansionHtml);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $this->data['databaseRow']['uid'] . '][' . $fieldName . ']');
$fullElement = $expansionHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="form-check t3-form-field-eval-null-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $expansionHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = (string)($config['placeholder'] ?? '');
if ($placeholder !== '') {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="form-check t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $expansionHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = $renderedLabel . '
<typo3-formengine-element-link class="formengine-field-item t3js-formengine-field-item" recordFieldId="' . htmlspecialchars($fieldId) . '">
' . $fieldInformationHtml . '
' . $fullElement . '
</typo3-formengine-element-link>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/link-element.js');
return $resultArray;
}
protected function getLinkExplanation(string $itemValue): array
{
if ($itemValue === '') {
return [];
}
$data = ['text' => '', 'icon' => ''];
$linkParts = $this->typoLinkCodecService->decode($itemValue);
try {
$linkData = $this->linkService->resolve($linkParts['url']);
} catch (FileDoesNotExistException|FolderDoesNotExistException|UnknownLinkHandlerException|InvalidPathException $e) {
return $data;
}
// Resolving the TypoLink parts (class, title, params)
$additionalAttributes = [];
foreach ($linkParts as $key => $value) {
if ($key === 'url') {
continue;
}
if ($value) {
$label = match ((string)$key) {
'class' => $this->getLanguageService()->sL('backend.browse_links:class'),
'title' => $this->getLanguageService()->sL('backend.browse_links:title'),
'additionalParams' => $this->getLanguageService()->sL('backend.browse_links:params'),
'rel' => $this->getLanguageService()->sL('backend.browse_links:linkRelationship'),
default => (string)$key
};
$additionalAttributes[] = '<span><strong>' . htmlspecialchars($label) . ': </strong> ' . htmlspecialchars($value) . '</span>';
}
}
$backendUser = $this->getBackendUser();
// Resolve the actual link
switch ($linkData['type']) {
case LinkService::TYPE_PAGE:
$pagePermissionClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
$pageRecord = BackendUtility::readPageAccess($linkData['pageuid'] ?? null, $pagePermissionClause);
// Is this a real page
if ($pageRecord['uid'] ?? 0) {
$fragmentTitle = '';
if (isset($linkData['fragment'])) {
if (MathUtility::canBeInterpretedAsInteger($linkData['fragment'])) {
$contentElement = BackendUtility::getRecord('tt_content', (int)$linkData['fragment'], '*', 'pid=' . $pageRecord['uid']);
if ($contentElement) {
$fragmentTitle = BackendUtility::getRecordTitle('tt_content', $contentElement, false, false);
}
}
$fragmentTitle = ' #' . ($fragmentTitle ?: $linkData['fragment']);
}
$data = [
'text' => $pageRecord['_thePathFull'] . '[' . $pageRecord['uid'] . ']' . $fragmentTitle,
'icon' => $this->iconFactory->getIconForRecord('pages', $pageRecord, IconSize::SMALL)->render(),
];
}
break;
case LinkService::TYPE_EMAIL:
$data = [
'text' => $linkData['email'] ?? '',
'icon' => $this->iconFactory->getIcon('content-elements-mailform', IconSize::SMALL)->render(),
];
break;
case LinkService::TYPE_URL:
$data = [
'text' => $linkData['url'] ?? '',
'icon' => $this->iconFactory->getIcon('apps-pagetree-page-shortcut-external', IconSize::SMALL)->render(),
];
break;
case LinkService::TYPE_FILE:
$file = $linkData['file'] ?? null;
if ($file instanceof File && $file->checkActionPermission('read') && !$file->getStorage()->isFallbackStorage()) {
$data = [
'text' => $file->getPublicUrl(),
'icon' => $this->iconFactory->getIconForFileExtension($file->getExtension(), IconSize::SMALL)->render(),
];
}
break;
case LinkService::TYPE_FOLDER:
$folder = $linkData['folder'] ?? null;
if ($folder instanceof Folder && $folder->checkActionPermission('read') && !$folder->getStorage()->isFallbackStorage()) {
$data = [
'text' => $folder->getPublicUrl(),
'icon' => $this->iconFactory->getIcon('apps-filetree-folder-default', IconSize::SMALL)->render(),
];
}
break;
case LinkService::TYPE_RECORD:
$table = $this->data['pageTsConfig']['TCEMAIN.']['linkHandler.'][$linkData['identifier'] . '.']['configuration.']['table'] ?? '';
$record = BackendUtility::getRecord($table, $linkData['uid']);
$pagePermissionClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
$hasPageAccess = BackendUtility::readPageAccess($record['pid'] ?? null, $pagePermissionClause) !== false;
if ($record && $hasPageAccess && $backendUser->check('tables_select', $table)) {
$recordTitle = BackendUtility::getRecordTitle($table, $record);
$tableTitle = $this->getLanguageService()->sL($this->data['tcaSchemata']->get($table)->getTitle());
$data = [
'text' => sprintf('%s [%s:%d]', $recordTitle, $tableTitle, $linkData['uid']),
'icon' => $this->iconFactory->getIconForRecord($table, $record, IconSize::SMALL)->render(),
];
} else {
$data = [
'text' => sprintf('%s', $linkData['uid']),
'icon' => $this->iconFactory->getIcon('tcarecords-' . $table . '-default', IconSize::SMALL, 'overlay-missing')->render(),
];
}
break;
case LinkService::TYPE_TELEPHONE:
$telephone = $linkData['telephone'];
if ($telephone) {
$data = [
'text' => $telephone,
'icon' => $this->iconFactory->getIcon('actions-device-mobile', IconSize::SMALL)->render(),
];
}
break;
case LinkService::TYPE_UNKNOWN:
$data = [
'text' => $linkData['file'] ?? $linkData['url'] ?? '',
'icon' => $this->iconFactory->getIcon('actions-link', IconSize::SMALL)->render(),
];
break;
default:
$data = [
'text' => 'not implemented type ' . $linkData['type'],
'icon' => '',
];
}
$data['additionalAttributes'] = $additionalAttributes !== []
? '
<div class="callout callout-info mt-3 mb-0">
<div class="callout-content">
<div class="callout-body">
' . implode(' - ', $additionalAttributes) . '
</div>
</div>
</div>
'
: ''; // Ensure "additionalAttributes" is always set (bw compatibility for the event)
return $this->eventDispatcher->dispatch(
new ModifyLinkExplanationEvent($data, $linkData, $linkParts, $this->data)
)->getLinkExplanation();
}
/**
* Initializes the LinkPopup FieldControl by processing the
* field specific configuration and by creating the necessary
* options array for the FieldControl.
*/
protected function initializeLinkPopup(array $fieldConfig): void
{
if (!($fieldConfig['appearance']['enableBrowser'] ?? true)) {
return;
}
$options = [];
if (is_array($fieldConfig['allowedTypes'] ?? null)
&& ($fieldConfig['allowedTypes'][0] ?? '') !== '*'
) {
$options['allowedTypes'] = $this->resolveAllowedTypes($fieldConfig['allowedTypes']);
}
if (is_array($fieldConfig['appearance']['allowedOptions'] ?? null)
&& ($fieldConfig['appearance']['allowedOptions'][0] ?? '') !== '*'
) {
$options['allowedOptions'] = $fieldConfig['appearance']['allowedOptions'];
}
if (is_array($fieldConfig['appearance']['allowedFileExtensions'] ?? null)
&& ($fieldConfig['appearance']['allowedFileExtensions'][0] ?? '') !== '*'
) {
$options['allowedFileExtensions'] = $fieldConfig['appearance']['allowedFileExtensions'];
}
if ($fieldConfig['appearance']['browserTitle'] ?? false) {
$options['title'] = $fieldConfig['appearance']['browserTitle'];
}
// Add the LinkPopup configuration to the field configuration
$this->data['parameterArray']['fieldConf']['config']['fieldControl']['linkPopup'] = [
'renderType' => 'linkPopup',
'options' => $options,
];
}
/**
* This method applies further processing to a given allow list
*/
protected function resolveAllowedTypes(array $allowedTypes): array
{
// First, remove duplicate entries
$allowedTypes = array_unique($allowedTypes);
// Replace "record" with available record link handlers
if (in_array('record', $allowedTypes, true)) {
unset($allowedTypes[(int)array_search('record', $allowedTypes, true)]);
$allowedTypes = array_merge($allowedTypes, $this->getRecordLinkHandlers());
}
// Return the resolves types, while removing duplicate entries
return array_unique($allowedTypes);
}
/**
* Returns the identifiers of link handlers, using the RecordLinkHandler class
*/
protected function getRecordLinkHandlers(): array
{
return $this->getLinkHandlerIdentifiers(
array_filter(
(array)($this->data['pageTsConfig']['TCEMAIN.']['linkHandler.'] ?? []),
static fn(array $handler): bool => ($handler['handler'] ?? '') === RecordLinkHandler::class
)
);
}
protected function getLinkHandlerIdentifiers(array $linkHandlers): array
{
return array_map(static fn(string $handler): string => trim($handler, '.'), array_keys($linkHandlers));
}
}
+221
View File
@@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Routing\UriBuilder;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderManifestInterface;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderPropertyManager;
use TYPO3\CMS\Core\Authentication\Mfa\MfaProviderRegistry;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
/**
* Renders an element, displaying MFA related information and providing
* interactions like deactivation of active providers and MFA in general.
*
* @internal
*/
class MfaInfoElement extends AbstractFormElement
{
private const array ALLOWED_TABLES = ['be_users', 'fe_users', 'be_users_settings'];
public function __construct(
private readonly IconFactory $iconFactory,
private readonly MfaProviderRegistry $mfaProviderRegistry,
private readonly UriBuilder $uriBuilder,
) {}
public function render(): array
{
$resultArray = $this->initializeResultArray();
$currentBackendUser = $this->getBackendUser();
$tableName = $this->data['tableName'];
// This renderType only works for user tables: be_users, fe_users
if (!in_array($tableName, self::ALLOWED_TABLES, true)) {
return $resultArray;
}
// Initialize a user based on the current table name
$targetUser = ($tableName === 'be_users' || $tableName === 'be_users_settings')
? GeneralUtility::makeInstance(BackendUserAuthentication::class)
: GeneralUtility::makeInstance(FrontendUserAuthentication::class);
$userId = (int)($this->data['databaseRow'][$targetUser->userid_column] ?? 0);
$targetUser->enablecolumns = ['deleted' => true];
$targetUser->setBeUserByUid($userId);
$isDeactivationAllowed = true;
// Providers from system maintainers can only be deactivated by system maintainers.
// However, this check is only necessary if the target is a backend user.
if (($targetUser instanceof BackendUserAuthentication)
&& $targetUser->isSystemMaintainer(true)
&& !$currentBackendUser->isSystemMaintainer()
) {
$isDeactivationAllowed = false;
}
// Fetch providers from the mfa field
$mfaProviders = json_decode($this->data['parameterArray']['itemFormElValue'] ?? '', true) ?? [];
$hasFormValue = $mfaProviders !== [];
// Initialize variables
$html = $childHtml = $activeProviders = $lockedProviders = $mfaSetupInfo = [];
$lang = $this->getLanguageService();
$enabledLabel = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.mfa.enabled'));
$disabledLabel = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.mfa.disabled'));
$status = '<span class="badge badge-danger badge-space-end t3js-mfa-status-label" data-alternative-label="' . $enabledLabel . '">' . $disabledLabel . '</span>';
// Unset invalid providers
foreach ($mfaProviders as $identifier => $providerSettings) {
if (!$this->mfaProviderRegistry->hasProvider($identifier)) {
unset($mfaProviders[$identifier]);
}
}
if ($mfaProviders !== []) {
// Check if remaining providers are active and/or locked for the user
foreach ($mfaProviders as $identifier => $providerSettings) {
$provider = $this->mfaProviderRegistry->getProvider($identifier);
$propertyManager = MfaProviderPropertyManager::create($provider, $targetUser);
if (!$provider->isActive($propertyManager)) {
continue;
}
$activeProviders[$identifier] = $provider;
if ($provider->isLocked($propertyManager)) {
$lockedProviders[] = $identifier;
}
}
if ($activeProviders !== []) {
// Change status label to MFA being enabled
$status = '<span class="badge badge-success badge-space-end t3js-mfa-status-label mb-2"' . ' data-alternative-label="' . $disabledLabel . '">' . $enabledLabel . '</span>';
$childHtml = $this->buildProviderListHtml($activeProviders, $lockedProviders, $isDeactivationAllowed);
}
} elseif (!$hasFormValue) {
// Fallback: read MFA state directly from the user when no form value was provided
$activeProviders = $this->mfaProviderRegistry->getActiveProviders($targetUser);
$lockedProviders = array_keys($this->mfaProviderRegistry->getLockedProviders($targetUser));
if ($activeProviders !== []) {
$status = '<span class="badge badge-success badge-space-end t3js-mfa-status-label mb-2"' . ' data-alternative-label="' . $disabledLabel . '">' . $enabledLabel . '</span>';
$childHtml = $this->buildProviderListHtml($activeProviders, $lockedProviders, false);
}
$mfaSetupInfo[] = '<div class="form-description">' . nl2br(htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/user_profile.xlf:mfa_providers.description'))) . '</div>';
if (!$this->mfaProviderRegistry->hasProviders()) {
$mfaSetupInfo[] = '<span class="badge badge-danger">' . htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/user_profile.xlf:mfa_providers.not_available')) . '</span>';
} else {
$mfaSetupInfo[] = '<div class="form-group"><div class="form-control-wrap t3js-file-controls">';
$mfaSetupInfo[] = '<a href="' . htmlspecialchars((string)$this->uriBuilder->buildUriFromRoute('mfa')) . '" class="btn btn-default">';
$mfaSetupInfo[] = htmlspecialchars($lang->sL('LLL:EXT:backend/Resources/Private/Language/user_profile.xlf:mfa_providers.' . ($activeProviders !== [] ? 'manage_link_title' : 'setup_link_title')));
$mfaSetupInfo[] = '</a>';
$mfaSetupInfo[] = '</div></div>';
}
}
$fieldId = 't3js-form-field-mfa-id' . StringUtility::getUniqueId('-');
$html[] = '<div class="formengine-field-item t3js-formengine-field-item" id="' . htmlspecialchars($fieldId) . '">';
$html[] = implode(PHP_EOL, $mfaSetupInfo);
$html[] = '<div class="form-control-wrap" style="max-width: ' . $this->formMaxWidth($this->defaultInputWidth) . 'px">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = implode(PHP_EOL, $childHtml);
if ($isDeactivationAllowed) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = '<button type="button"';
$html[] = ' class="t3js-deactivate-mfa-button btn btn-danger mt-2 ' . ($activeProviders === [] ? 'disabled" disabled="disabled' : '') . '"';
$html[] = ' data-confirmation-title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.deactivateMfa')) . '"';
$html[] = ' data-confirmation-content="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.deactivateMfa.confirmation.text')) . '"';
$html[] = ' data-confirmation-cancel-text="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel')) . '"';
$html[] = ' data-confirmation-deactivate-text="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deactivate')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-toggle-off', IconSize::SMALL)->render('inline');
$html[] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.deactivateMfa'));
$html[] = '</button>';
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
// JavaScript is not needed in case deactivation is not allowed or no active providers exist
if ($isDeactivationAllowed && $activeProviders !== []) {
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/mfa-info-element.js'
)->instance('#' . $fieldId, ['userId' => $userId, 'tableName' => $tableName]);
}
$resultArray['html'] = $this->wrapWithFieldsetAndLegend($status . implode(PHP_EOL, $html));
return $resultArray;
}
/**
* Build the HTML list of active MFA providers with their status badges
* and optional deactivation buttons.
*
* @param MfaProviderManifestInterface[] $activeProviders
* @param string[] $lockedProviders
* @return string[]
*/
private function buildProviderListHtml(
array $activeProviders,
array $lockedProviders,
bool $isDeactivationAllowed,
): array {
$childHtml = [];
$lang = $this->getLanguageService();
$childHtml[] = '<ul class="list-group t3js-mfa-active-providers-list">';
foreach ($activeProviders as $identifier => $activeProvider) {
$childHtml[] = '<li class="list-group-item" id="provider-' . htmlspecialchars((string)$identifier) . '">';
$childHtml[] = $this->iconFactory->getIcon($activeProvider->getIconIdentifier(), IconSize::SMALL);
$childHtml[] = htmlspecialchars($lang->sL($activeProvider->getTitle()));
if (in_array($identifier, $lockedProviders, true)) {
$childHtml[] = '<span class="badge badge-danger">' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.locked')) . '</span>';
} else {
$childHtml[] = '<span class="badge badge-success">' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.active')) . '</span>';
}
if ($isDeactivationAllowed) {
$childHtml[] = '<button type="button"';
$childHtml[] = ' class="btn btn-default btn-sm float-end t3js-deactivate-provider-button"';
$childHtml[] = ' data-confirmation-title="' . htmlspecialchars(sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.deactivateMfaProvider'), $lang->sL($activeProvider->getTitle()))) . '"';
$childHtml[] = ' data-confirmation-content="' . htmlspecialchars(sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.deactivateMfaProvider.confirmation.text'), $lang->sL($activeProvider->getTitle()))) . '"';
$childHtml[] = ' data-confirmation-cancel-text="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel')) . '"';
$childHtml[] = ' data-confirmation-deactivate-text="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.deactivate')) . '"';
$childHtml[] = ' data-provider="' . htmlspecialchars((string)$identifier) . '"';
$childHtml[] = ' title="' . htmlspecialchars(sprintf($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:buttons.deactivateMfaProvider'), $lang->sL($activeProvider->getTitle()))) . '"';
$childHtml[] = '>';
$childHtml[] = $this->iconFactory->getIcon('actions-delete', IconSize::SMALL)->render('inline');
$childHtml[] = '</button>';
}
$childHtml[] = '</li>';
}
$childHtml[] = '</ul>';
return $childHtml;
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* None element is a "disabled" input element with formatted values if needed.
*/
class NoneElement extends AbstractFormElement
{
/**
* @var int
*/
protected $minimumInputWidth = 5;
/**
* This will render a non-editable display of the content of the field.
*
* @return array The HTML code for the TCEform field
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$itemValue = $parameterArray['itemFormElValue'];
if ($config['format'] ?? false) {
$formatOptions = $config['format.'] ?? [];
$itemValue = $this->formatValue($config['format'], $itemValue, $formatOptions);
}
$size = $config['size'] ?? $this->defaultInputWidth;
$size = MathUtility::forceIntegerInRange($size, $this->minimumInputWidth, $this->maxInputWidth);
$width = $this->formMaxWidth($size);
$fieldId = StringUtility::getUniqueId('formengine-textarea-');
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" value="' . htmlspecialchars($itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
}
+291
View File
@@ -0,0 +1,291 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* type=number element.
*
* The NumberElement renders a html field with the type "number" attribute.
*/
class NumberElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a single-line input form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$format = $config['format'] ?? 'integer';
if ($format !== 'integer' && $format !== 'decimal') {
throw new \UnexpectedValueException(
'Format "' . $format . '" for field "' . $fieldName . '" in table "' . $table . '" is '
. 'not valid. Must be either empty or set to one of: "integer", "decimal".',
1649124682
);
}
// @todo This should be configurable (e.g. [config][precision])
$precision = 2;
$itemValue = $parameterArray['itemFormElValue'];
$width = $this->formMaxWidth(
MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth)
);
$fieldId = StringUtility::getUniqueId('formengine-input-');
$itemName = (string)$parameterArray['itemFormElName'];
$renderedLabel = $this->renderLabel($fieldId);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" name="' . htmlspecialchars($itemName) . '" value="' . htmlspecialchars((string)$itemValue) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$languageService = $this->getLanguageService();
// Always add the format.
$evalList = [$format];
if ($config['nullable'] ?? false) {
$evalList[] = 'null';
}
$attributes = [
'value' => '',
'id' => $fieldId,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => (string)json_encode([
'field' => $itemName,
'evalList' => implode(',', $evalList),
], JSON_THROW_ON_ERROR),
'data-formengine-input-name' => $itemName,
];
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
if (isset($config['autocomplete'])) {
$attributes['autocomplete'] = empty($config['autocomplete']) ? 'new-' . $fieldName : 'on';
}
$valueSliderHtml = [];
if (is_array($config['slider'] ?? false)) {
if ($format === 'decimal') {
$itemValue = (float)$itemValue;
} else {
$itemValue = (int)$itemValue;
}
$valueSliderConfiguration = [
'precision' => (string)$precision,
'format' => $format,
'linked-field' => '[data-formengine-input-name="' . $itemName . '"]',
];
$rangeAttributes = [
'type' => 'range',
'class' => 'form-range-input',
'min' => (string)(float)($config['range']['lower'] ?? 0),
'max' => (string)(float)($config['range']['upper'] ?? 10000),
'step' => (string)($config['slider']['step'] ?? 1),
'style' => 'width: ' . (int)($config['slider']['width'] ?? 400) . 'px',
'title' => (string)$itemValue,
'value' => (string)$itemValue,
];
$valueSliderHtml[] = '<typo3-formengine-valueslider ' . GeneralUtility::implodeAttributes($valueSliderConfiguration, true) . '>';
$valueSliderHtml[] = '<div class="form-range">';
$valueSliderHtml[] = '<input ' . GeneralUtility::implodeAttributes($rangeAttributes, true) . '>';
$valueSliderHtml[] = '</div>';
$valueSliderHtml[] = '</typo3-formengine-valueslider>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-wizard/value-slider.js');
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
if (isset($config['range']['lower'])) {
$attributes['min'] = (string)(float)$config['range']['lower'];
}
if (isset($config['range']['upper'])) {
$attributes['max'] = (string)(float)$config['range']['upper'];
}
if ($format === 'decimal') {
$attributes['step'] = '0.' . str_repeat('0', $precision - 1) . '1';
}
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-item-element">';
if (is_array($config['valuePicker']['items'] ?? false)) {
$attributes['class'] = 'form-control';
$mainFieldHtml[] = '<typo3-backend-combobox>';
$mainFieldHtml[] = '<input type="number" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
foreach ($config['valuePicker']['items'] as $item) {
$mainFieldHtml[] = '<typo3-backend-combobox-choice value="' . htmlspecialchars((string)$item['value']) . '">' . htmlspecialchars($languageService->sL($item['label'])) . '</typo3-backend-combobox-choice>';
}
$mainFieldHtml[] = '</typo3-backend-combobox>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/element/combobox-element.js');
} else {
$attributes['class'] = implode(' ', [
'form-control',
'form-control-clearable',
't3js-clearable',
]);
$mainFieldHtml[] = '<input type="number" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
}
$mainFieldHtml[] = '<input type="hidden" name="' . $itemName . '" value="' . htmlspecialchars((string)$itemValue) . '" />';
$mainFieldHtml[] = '</div>';
if (!empty($valueSliderHtml) || !empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$mainFieldHtml[] = implode(LF, $valueSliderHtml);
if (!empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="btn-group">' . $fieldControlHtml . '</div>';
}
$mainFieldHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $this->data['databaseRow']['uid'] . '][' . $fieldName . ']');
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="form-check t3-form-field-eval-null-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = trim((string)($config['placeholder'] ?? ''));
if ($placeholder !== '') {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="form-check t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = $renderedLabel . '
<div class="formengine-field-item t3js-formengine-field-item">
' . $fieldInformationHtml . $fullElement . '
</div>';
return $resultArray;
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
/**
* PassThroughElement is the dummy element for type="passthrough".
* It does not render anything.
*/
class PassThroughElement extends AbstractFormElement
{
/**
* Return the empty initialized result array
*/
public function render(): array
{
return $this->initializeResultArray();
}
}
+274
View File
@@ -0,0 +1,274 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyAction;
use TYPO3\CMS\Core\PasswordPolicy\PasswordPolicyValidator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* General type=password element.
*/
class PasswordElement extends AbstractFormElement
{
/**
* This will render a single-line password form field, possibly with various control/validation features
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$config = $parameterArray['fieldConf']['config'];
$passwordPolicyValidator = null;
$itemValue = $parameterArray['itemFormElValue'];
$width = $this->formMaxWidth(
MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth)
);
$fieldId = StringUtility::getUniqueId('formengine-input-');
$itemName = (string)$parameterArray['itemFormElName'];
$renderedLabel = $this->renderLabel($fieldId);
$passwordPolicy = $config['passwordPolicy'] ?? null;
if ($passwordPolicy) {
// We always use PasswordPolicyAction::NEW_USER_PASSWORD here, since the password is not set by the user,
// but either by an admin or an editor
$passwordPolicyValidator = GeneralUtility::makeInstance(
PasswordPolicyValidator::class,
PasswordPolicyAction::NEW_USER_PASSWORD,
is_string($passwordPolicy) ? $passwordPolicy : ''
);
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<input class="form-control" id="' . htmlspecialchars($fieldId) . '" name="' . htmlspecialchars($itemName) . '" value="' . htmlspecialchars($this->getObfuscatedSecretValue($itemValue)) . '" type="text" disabled>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$languageService = $this->getLanguageService();
// Always add "trim" and "password" (required for JS validation)
$evalList = ['trim', 'password'];
if ($config['nullable'] ?? false) {
$evalList[] = 'null';
}
$attributes = [
'value' => '',
'id' => $fieldId,
'spellcheck' => 'false',
'class' => implode(' ', [
'form-control',
'form-control-clearable',
't3js-clearable',
]),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-params' => (string)json_encode([
'field' => $itemName,
'evalList' => implode(',', $evalList),
], JSON_THROW_ON_ERROR),
'data-formengine-input-name' => $itemName,
];
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
$attributes['autocomplete'] = ($config['autocomplete'] ?? false) ? 'current-password' : 'new-password';
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-item-element">';
$mainFieldHtml[] = '<input type="password" ' . GeneralUtility::implodeAttributes($attributes, true) . ' />';
$mainFieldHtml[] = '<input type="hidden" disabled data-enable-on-modification="true" name="' . $itemName . '" value="' . htmlspecialchars($this->getObfuscatedSecretValue($itemValue)) . '" />';
$mainFieldHtml[] = '</div>';
if (!empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $this->data['databaseRow']['uid'] . '][' . $fieldName . ']');
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="form-check t3-form-field-eval-null-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = trim($config['placeholder'] ?? '');
if ($placeholder !== '') {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$fullElement = [];
$fullElement[] = '<div class="form-check t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap" style="max-width:' . $width . 'px">';
$fullElement[] = '<input type="text" class="form-control" disabled="disabled" value="' . htmlspecialchars($shortenedPlaceholder) . '" />';
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$passwordPolicyInfo = '';
if ($passwordPolicy) {
$passwordPolicyInfo = $this->renderPasswordPolicyRequirements($passwordPolicyValidator, $fieldId);
}
$passwordElementAttributes['class'] = 'formengine-field-item t3js-formengine-field-item';
$passwordElementAttributes['recordFieldId'] = $fieldId;
$passwordElementAttributes['passwordPolicy'] = $passwordPolicy;
$resultArray['html'] = $renderedLabel . '
<typo3-formengine-element-password ' . GeneralUtility::implodeAttributes($passwordElementAttributes, true) . '>
' . $fieldInformationHtml . '
' . $fullElement . '
' . $passwordPolicyInfo . '
</typo3-formengine-element-password>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/password-element.js'
);
return $resultArray;
}
private function renderPasswordPolicyRequirements(
PasswordPolicyValidator $passwordPolicyValidator,
string $fieldId
): string {
if (empty($passwordPolicyValidator->getRequirements())) {
return '';
}
$passwordPolicyElement = [];
$requirements = [];
foreach ($passwordPolicyValidator->getRequirements() as $id => $requirement) {
$requirements[] = '<li data-id="' . htmlspecialchars($fieldId . '-' . $id) . '">' . $requirement . '</li>';
}
$calloutTitle = $this->getLanguageService()->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_password_policy.xlf:passwordRequirements.description'
);
$passwordPolicyElement[] = '<div id="password-policy-info-' . htmlspecialchars($fieldId) . '" class="mt-2 callout callout-secondary hidden">';
$passwordPolicyElement[] = ' <div class="callout-content">';
$passwordPolicyElement[] = ' <div class="callout-title">' . htmlspecialchars($calloutTitle) . '</div>';
$passwordPolicyElement[] = ' <div class="callout-body">';
$passwordPolicyElement[] = ' <ul>';
$passwordPolicyElement[] = implode(LF, $requirements);
$passwordPolicyElement[] = ' </ul>';
$passwordPolicyElement[] = ' </div>';
$passwordPolicyElement[] = ' </div>';
$passwordPolicyElement[] = '</div>';
return implode(LF, $passwordPolicyElement);
}
/**
* Obfuscated a (hashed) password secret with a static string.
*
* @todo
* + server-side password obfuscation value is `*********` (9 chars)
* + client-side password obfuscation value is `********` (8 chars)
*/
protected function getObfuscatedSecretValue(?string $value): string
{
if ($value === null || $value === '') {
return '';
}
return '*********';
}
}
+112
View File
@@ -0,0 +1,112 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render elements of type="radio".
*/
class RadioElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a series of radio buttons.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$disabled = '';
if ($this->data['parameterArray']['fieldConf']['config']['readOnly'] ?? false) {
$disabled = ' disabled';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
foreach ($this->data['parameterArray']['fieldConf']['config']['items'] as $itemNumber => $itemLabelAndValue) {
$label = $itemLabelAndValue['label'];
$value = $itemLabelAndValue['value'];
$radioId = htmlspecialchars(StringUtility::getUniqueId('formengine-radio-') . '-' . $itemNumber);
$radioElementAttrs = array_merge(
[
'type' => 'radio',
'id' => $radioId,
'value' => $value,
'class' => 'form-check-input',
'name' => $this->data['parameterArray']['itemFormElName'],
],
$this->getOnFieldChangeAttrs('click', $this->data['parameterArray']['fieldChangeFunc'] ?? [])
);
if ((string)$value === (string)$this->data['parameterArray']['itemFormElValue']) {
$radioElementAttrs['checked'] = 'checked';
}
$html[] = '<div class="form-check' . $disabled . '">';
$html[] = '<input ' . GeneralUtility::implodeAttributes($radioElementAttrs, true, true) . $disabled . '>';
$html[] = '<label class="form-check-label" for="' . $radioId . '">';
$html[] = htmlspecialchars($this->appendValueToLabelInDebugMode($label, $value));
$html[] = '</label>';
$html[] = '</div>';
}
$html[] = '</div>';
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
}
@@ -0,0 +1,343 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Creates a widget with check box elements.
*
* This is rendered for config type=select, renderType=selectCheckBox
*/
class SelectCheckBoxElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
) {}
/**
* Render check boxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
// Field configuration from TCA:
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$readOnly = (bool)($config['readOnly'] ?? false);
$selectItems = $config['items'] ?? [];
if (empty($selectItems)) {
// Early return in case the field does not contain any items
return $resultArray;
}
// Get item value as array and make unique, which is fine because there can be no duplicates anyway.
$itemArray = array_flip($parameterArray['itemFormElValue']);
// Initialize variables and traverse the items
$groups = [];
$currentGroup = 0;
$counter = 0;
$elementId = StringUtility::getUniqueId('formengine-select-checkbox-');
foreach ($selectItems as $item) {
// Non-selectable element:
if ($item['value'] === '--div--') {
$selIcon = '';
if (isset($item['icon']) && $item['icon'] !== 'empty-empty') {
$selIcon = FormEngineUtility::getIconHtml($item['icon']);
}
$currentGroup++;
$groups[$currentGroup]['header'] = [
'icon' => $selIcon,
'title' => $item['label'],
];
} else {
// Check if some help text is available
// Help text is expected to be an associative array
// with two key, "title" and "description"
// For the sake of backwards compatibility, we test if the help text
// is a string and use it as a description (this could happen if items
// are modified with an itemsProcFunc)
$help = '';
if (!empty($item['description'])) {
if (is_array($item['description'])) {
$helpArray = $item['description'];
} else {
$helpArray['description'] = $item['description'];
}
$help = $this->wrapInHelp($helpArray);
}
// Check if current item is selected. If found, unset the key in the $itemArray.
$checked = isset($itemArray[$item['value']]);
if ($checked) {
unset($itemArray[$item['value']]);
}
// Build item array
$groups[$currentGroup]['items'][] = [
'id' => $elementId . '-item-' . $counter,
'name' => $parameterArray['itemFormElName'] . '[' . $counter . ']',
'value' => $item['value'],
'checked' => $checked,
'icon' => FormEngineUtility::getIconHtml(!empty($item['icon']) ? $item['icon'] : 'empty-empty'),
'title' => $item['label'],
'help' => $help,
];
$counter++;
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item" data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
if (!$readOnly) {
// Add an empty hidden field which will send a blank value if all items are unselected.
$html[] = '<input type="hidden" class="select-checkbox" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
}
// Building the checkboxes
foreach ($groups as $groupKey => $group) {
$groupId = htmlspecialchars($elementId . '-group-' . $groupKey);
$groupCollapsibleId = $groupId . '-collapse';
$hasGroupHeader = is_array($group['header'] ?? false);
$html[] = '<div id="' . $groupId . '" class="panel panel-default" data-multi-record-selection-identifier="' . $groupId . '">';
if ($hasGroupHeader) {
$expanded = ($config['appearance']['expandAll'] ?? false);
$html[] = '<div class="panel-heading" role="tab">';
$html[] = '<div class="panel-heading-row">';
$html[] = '<button type="button" class="panel-button' . (!$expanded ? ' collapsed' : '') . '" aria-expanded="' . ($expanded ? 'true' : 'false') . '"';
$html[] = ' aria-controls="' . $groupCollapsibleId . '" data-bs-target="#' . $groupCollapsibleId . '" data-bs-toggle="collapse">';
$html[] = '<div class="panel-icon">';
$html[] = $group['header']['icon'];
$html[] = '</div>';
$html[] = '<div class="panel-title">';
$html[] = htmlspecialchars($group['header']['title']);
$html[] = '</div>';
$html[] = '<span class="caret"></span>';
$html[] = '</button>';
$html[] = '</div>';
$html[] = '</div>';
}
if (is_array($group['items'] ?? null)) {
$tableRows = [];
// Render rows
foreach ($group['items'] as $item) {
$inputElementAttrs = [
'type' => 'checkbox',
'class' => 'form-check-input t3js-multi-record-selection-check',
'id' => $item['id'],
'name' => $item['name'],
'value' => $item['value'],
];
if ($item['checked']) {
$inputElementAttrs['checked'] = 'checked';
}
if ($readOnly) {
// Disable item if the element is readonly
$inputElementAttrs['disabled'] = 'disabled';
} else {
// Add fieldChange attributes if element is not readOnly
$inputElementAttrs = array_merge(
$inputElementAttrs,
$this->getOnFieldChangeAttrs('click', $parameterArray['fieldChangeFunc'] ?? [])
);
}
$tableRows[] = '<tr data-multi-record-selection-element="true">';
$tableRows[] = '<td class="col-checkbox">';
$tableRows[] = '<span class="form-check form-check-type-toggle">';
$tableRows[] = '<input ' . GeneralUtility::implodeAttributes($inputElementAttrs, true, true) . '>';
$tableRows[] = '</span>';
$tableRows[] = '</td>';
$tableRows[] = '<td class="col-title">';
$tableRows[] = '<label class="label-block nowrap-disabled" for="' . $item['id'] . '">';
$tableRows[] = '<span class="inline-icon">' . $item['icon'] . '</span>';
$tableRows[] = htmlspecialchars($this->appendValueToLabelInDebugMode($item['title'], $item['value']), ENT_COMPAT, 'UTF-8', false);
$tableRows[] = '</label>';
$tableRows[] = '</td>';
$tableRows[] = '<td class="text-end">' . $item['help'] . '</td>';
$tableRows[] = '</tr>';
}
if ($hasGroupHeader) {
$expandAll = ($config['appearance']['expandAll'] ?? false) ? 'show' : '';
$html[] = '<div id="' . $groupCollapsibleId . '" class="panel-collapse collapse ' . $expandAll . '" role="tabpanel">';
}
$html[] = '<div class="table-fit">';
$html[] = '<table class="table table-hover">';
if (!$readOnly) {
// Add table header with actions, in case the element is not readOnly
$html[] = '<thead>';
$html[] = '<tr>';
$html[] = '<th class="col-checkbox">' . $this->getRecordSelectionCheckActions() . '</th>';
$html[] = '<th class="col-title" colspan="2">' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.th.name')) . '</th>';
$html[] = '</tr>';
$html[] = '</thead>';
// Add JavaScript module. This is only needed, in case the element
// is not readOnly, since otherwise no checkbox changes take place.
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/multi-record-selection.js');
}
$html[] = '<tbody>' . implode(LF, $tableRows) . '</tbody>';
$html[] = '</table>';
$html[] = '</div>';
if ($hasGroupHeader) {
$html[] = '</div>';
}
}
$html[] = '</div>';
}
$html[] = '</div>';
if (!$readOnly && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
/**
* A function that creates an icon with a help text. If a user clicks on
* the icon, the help text will show up as tooltip
*
* @param array $overloadHelpText Array with text to overload help text
* @return string the HTML code ready to render
*/
protected function wrapInHelp(array $overloadHelpText = []): string
{
// If there's a help text or some overload information, proceed with preparing an output
if (empty($overloadHelpText)) {
return '';
}
$text = $this->iconFactory->getIcon('actions-system-help-open', IconSize::SMALL)->render();
$abbrClassAdd = ' help-teaser-icon';
$text = '<abbr class="help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
$wrappedText = '<span class="help-link" data-bs-content="<p></p>"';
// The overload array may provide a title and a description
// If either one is defined, add them to the "data" attributes
if (isset($overloadHelpText['title'])) {
$wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
}
if (isset($overloadHelpText['description'])) {
$wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
}
$wrappedText .= '>' . $text . '</span>';
return $wrappedText;
}
protected function getRecordSelectionCheckActions(): string
{
$lang = $this->getLanguageService();
return '
<div class="dropdown">
<button type="button" class="dropdown-toggle dropdown-toggle-link t3js-multi-record-selection-check-actions-toggle" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false" aria-label="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.openSelectionOptions')) . '">
' . $this->iconFactory->getIcon('actions-selection', IconSize::SMALL)->render() . '
</button>
<ul class="dropdown-menu t3js-multi-record-selection-check-actions">
<li>
<button type="button" class="dropdown-item" disabled data-multi-record-selection-check-action="check-all" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-all', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll')) . '
</span>
</span>
</button>
</li>
<li>
<button type="button" class="dropdown-item" disabled data-multi-record-selection-check-action="check-none" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-none', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll')) . '
</span>
</span>
</button>
</li>
<li>
<button type="button" class="dropdown-item" data-multi-record-selection-check-action="toggle" title="' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-invert', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection')) . '
</span>
</span>
</button>
</li>
</ul>
</div>';
}
}
@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Creates a widget where only one item can be selected.
* This is either a select drop-down if no size config is given or set to 1, or a select box.
*
* This is rendered for type=country
*/
final class SelectCountryElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'selectIcons' => [
'renderType' => 'selectIcons',
'disabled' => true,
],
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
'after' => [
'selectIcons',
],
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [ 'localizationStateSelector' ],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [ 'otherLanguageContent' ],
],
];
/**
* Render single element
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$selectItems = $parameterArray['fieldConf']['config']['items'] ?? [];
$classList = ['form-select', 'form-control-adapt'];
// Initialization:
$selectId = StringUtility::getUniqueId('tceforms-select-');
$selectedIcon = '';
$size = (int)($config['size'] ?? 0);
// Style set on <select/>
$options = '';
$disabled = false;
if (!empty($config['readOnly'])) {
$disabled = true;
}
// Prepare groups
$selectItemCounter = 0;
$selectItemGroupCount = 0;
$selectItemGroups = [];
$selectedValue = '';
$hasIcons = false;
// In case e.g. "l10n_display" is set to "defaultAsReadonly" only one value (as string) could be handed in
if (!empty($parameterArray['itemFormElValue'])) {
$selectedValue = (string)$parameterArray['itemFormElValue'];
}
foreach ($selectItems as $item) {
$selected = $selectedValue === (string)$item['value'];
if ($item['value'] === '--div--') {
// IS OPTGROUP
if ($selectItemCounter !== 0) {
$selectItemGroupCount++;
}
$selectItemGroups[$selectItemGroupCount]['header'] = [
'title' => $item['label'],
];
} else {
$icon = !empty($item['icon']) ? FormEngineUtility::getIconHtml($item['icon'], $item['label'], $item['label']) : '';
if ($selected) {
$selectedIcon = $icon;
}
$selectItemGroups[$selectItemGroupCount]['items'][] = [
'title' => $this->appendValueToLabelInDebugMode($item['label'], $item['value']),
'value' => $item['value'],
'icon' => $icon,
'selected' => $selected,
];
$selectItemCounter++;
}
}
// Fallback icon
// @todo: assign a special icon for non matching values?
if (!$selectedIcon && !empty($selectItemGroups[0]['items'][0]['icon'])) {
$selectedIcon = $selectItemGroups[0]['items'][0]['icon'];
}
// Process groups
foreach ($selectItemGroups as $selectItemGroup) {
// suppress groups without items
if (empty($selectItemGroup['items'])) {
continue;
}
$optionGroup = is_array($selectItemGroup['header'] ?? null);
$options .= ($optionGroup ? '<optgroup label="' . htmlspecialchars($selectItemGroup['header']['title'], ENT_COMPAT, 'UTF-8', false) . '">' : '');
foreach ($selectItemGroup['items'] as $item) {
$options .= '<option value="' . htmlspecialchars($item['value']) . '" data-icon="'
. htmlspecialchars($item['icon']) . '"'
. ($item['selected'] ? ' selected="selected"' : '') . '>' . htmlspecialchars($item['title'], ENT_COMPAT, 'UTF-8', false) . '</option>';
}
$hasIcons = !empty($item['icon']);
$options .= ($optionGroup ? '</optgroup>' : '');
}
$selectAttributes = [
'id' => $selectId,
'name' => (string)($parameterArray['itemFormElName'] ?? ''),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'class' => implode(' ', $classList),
];
if ($size) {
$selectAttributes['size'] = (string)$size;
}
if ($disabled) {
$selectAttributes['disabled'] = 'disabled';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = $this->renderLabel($selectId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-element">';
if ($hasIcons) {
$html[] = '<div class="input-group">';
$html[] = '<span class="input-group-text input-group-icon">';
$html[] = $selectedIcon;
$html[] = '</span>';
}
$html[] = '<select ' . GeneralUtility::implodeAttributes($selectAttributes, true) . '>';
$html[] = $options;
$html[] = '</select>';
if ($hasIcons) {
$html[] = '</div>';
}
$html[] = '</div>';
if (!$disabled && !empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-items-aside form-wizards-items-aside--field-control">';
$html[] = '<div class="btn-group">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-items-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$onFieldChangeItems = $this->getOnFieldChangeItems($parameterArray['fieldChangeFunc'] ?? []);
$resultArray['javaScriptModules']['selectSingleElement'] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/select-country-element.js'
)->invoke('initializeOnReady', '#' . $selectId, ['onChange' => $onFieldChangeItems]);
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
}
@@ -0,0 +1,482 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render a widget with two boxes side by side.
*
* This is rendered for config type=select, renderType=selectMultipleSideBySide set
*/
class SelectMultipleSideBySideElement extends AbstractFormElement
{
/**
* Default field controls for this element.
*
* @var array
*/
protected $defaultFieldControl = [
'editPopup' => [
'renderType' => 'editPopup',
'disabled' => true,
],
'addRecord' => [
'renderType' => 'addRecord',
'disabled' => true,
],
'listModule' => [
'renderType' => 'listModule',
'disabled' => true,
'after' => [ 'addRecord' ],
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
public function __construct(
private readonly IconFactory $iconFactory,
) {}
/**
* Merge field control configuration with default controls and render them.
*
* @return array Result array
*/
protected function renderFieldControl(): array
{
$alternativeResult = [
'additionalInlineLanguageLabelFiles' => [],
'stylesheetFiles' => [],
'javaScriptModules' => [],
'inlineData' => [],
'html' => '',
];
$options = $this->data;
$fieldControl = $this->defaultFieldControl;
$fieldControlFromTca = $options['parameterArray']['fieldConf']['config']['fieldControl'] ?? [];
ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca);
$options['renderType'] = 'fieldControl';
if (isset($fieldControl['editPopup'])) {
$editPopupControl = $fieldControl['editPopup'];
unset($fieldControl['editPopup']);
$alternativeOptions = $options;
$alternativeOptions['renderData']['fieldControl'] = ['editPopup' => $editPopupControl];
$alternativeResult = $this->nodeFactory->create($alternativeOptions)->render();
}
$options['renderData']['fieldControl'] = $fieldControl;
return [$this->nodeFactory->create($options)->render(), $alternativeResult];
}
/**
* Render side by side element.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
if ($config['readOnly'] ?? false) {
// Early return for the relatively simple read only case
return $this->renderReadOnly();
}
$filterTextfield = [];
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$elementName = $parameterArray['itemFormElName'];
$possibleItems = $config['items'];
$selectedItems = $parameterArray['itemFormElValue'] ?: [];
$maxItems = $config['maxitems'];
$size = (int)($config['size'] ?? 2);
$autoSizeMax = (int)($config['autoSizeMax'] ?? 0);
if ($autoSizeMax > 0) {
$size = MathUtility::forceIntegerInRange($size, 1);
$size = MathUtility::forceIntegerInRange(count($selectedItems) + 1, $size, $autoSizeMax);
}
$itemCanBeSelectedMoreThanOnce = !empty($config['multiple']);
$listOfSelectedValues = [];
$selectedItemsHtml = [];
foreach ($selectedItems as $itemValue) {
foreach ($possibleItems as $possibleItem) {
if ($possibleItem['value'] == $itemValue) {
$title = $possibleItem['label'];
$listOfSelectedValues[] = $itemValue;
$selectedItemsHtml[] = '<option value="' . htmlspecialchars((string)$itemValue) . '" title="' . htmlspecialchars((string)$title) . '">' . htmlspecialchars($this->appendValueToLabelInDebugMode($title, $itemValue)) . '</option>';
break;
}
}
}
$selectableItemCounter = 0;
$selectableItemGroupCounter = 0;
$selectableItemGroups = [];
$selectableItemsHtml = [];
// Initialize groups
foreach ($possibleItems as $possibleItem) {
$disableAttributes = [];
if (!$itemCanBeSelectedMoreThanOnce && in_array((string)$possibleItem['value'], $selectedItems, true)) {
$disableAttributes = [
'disabled' => 'disabled',
'class' => 'hidden',
];
}
if ($possibleItem['value'] === '--div--') {
if ($selectableItemCounter !== 0) {
$selectableItemGroupCounter++;
}
$selectableItemGroups[$selectableItemGroupCounter]['header']['title'] = $possibleItem['label'];
} else {
$selectableItemGroups[$selectableItemGroupCounter]['items'][] = [
'label' => $this->appendValueToLabelInDebugMode($possibleItem['label'], $possibleItem['value']),
'attributes' => array_merge(['title' => $possibleItem['label'], 'value' => $possibleItem['value']], $disableAttributes),
];
// In case the item is not disabled, enable the group (if any)
if ($disableAttributes === [] && isset($selectableItemGroups[$selectableItemGroupCounter]['header'])) {
$selectableItemGroups[$selectableItemGroupCounter]['header']['disabled'] = false;
}
$selectableItemCounter++;
}
}
// Process groups
foreach ($selectableItemGroups as $selectableItemGroup) {
if (!is_array($selectableItemGroup['items'] ?? false)) {
continue;
}
$optionGroup = isset($selectableItemGroup['header']);
if ($optionGroup) {
$selectableItemsHtml[] = '<optgroup label="' . htmlspecialchars($selectableItemGroup['header']['title']) . '"' . (($selectableItemGroup['header']['disabled'] ?? true) ? 'class="hidden" disabled="disabled"' : '') . '>';
}
foreach ($selectableItemGroup['items'] as $item) {
$selectableItemsHtml[] = '
<option ' . GeneralUtility::implodeAttributes($item['attributes'], true, true) . '>
' . htmlspecialchars($item['label']) . '
</option>';
}
if ($optionGroup) {
$selectableItemsHtml[] = '</optgroup>';
}
}
// Html stuff for filter and select filter on top of right side of multi select boxes
$filterTextfieldId = StringUtility::getUniqueId('tceforms-multiselect-filter-');
$filterTextfield[] = '<input type="search" id="' . $filterTextfieldId . '" autocomplete="off" class="t3js-formengine-multiselect-filter-textfield form-control" value="">';
$filterDropDownOptions = [];
if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) {
foreach ($config['multiSelectFilterItems'] as $optionElement) {
$value = $languageService->sL($optionElement[0]);
$label = $value;
if (isset($optionElement[1]) && trim($optionElement[1]) !== '') {
$label = $languageService->sL($optionElement[1]);
}
$filterDropDownOptions[] = '<option value="' . htmlspecialchars($value) . '">' . htmlspecialchars($label) . '</option>';
}
}
$filterHtml = [];
$filterHtml[] = '<div class="form-wizards-item-filter">';
if (!empty($filterDropDownOptions)) {
$filterHtml[] = '<div class="t3js-formengine-multiselect-filter-container form-multigroup-wrap">';
$filterHtml[] = '<div class="form-multigroup-item">';
$filterHtml[] = '<select class="form-select t3js-formengine-multiselect-filter-dropdown">';
$filterHtml[] = implode(LF, $filterDropDownOptions);
$filterHtml[] = '</select>';
$filterHtml[] = '</div>';
$filterHtml[] = '<div class="form-multigroup-item">';
$filterHtml[] = implode(LF, $filterTextfield);
$filterHtml[] = '</div>';
$filterHtml[] = '</div>';
} else {
$filterHtml[] = implode(LF, $filterTextfield);
}
$filterHtml[] = '</div>';
$multipleAttribute = '';
if ($maxItems !== 1 && $size !== 1) {
$multipleAttribute = ' multiple="multiple"';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
[$fieldControlResult, $alternativeControlResult] = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$alternativeFieldControlHtml = $alternativeControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $alternativeControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$selectedOptionsFieldId = StringUtility::getUniqueId('tceforms-multiselect-');
$availableOptionsFieldId = StringUtility::getUniqueId('tceforms-multiselect-');
$html = [];
$html[] = $this->renderLabel($selectedOptionsFieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<input type="hidden" data-formengine-input-name="' . htmlspecialchars($elementName) . '" value="' . (int)$itemCanBeSelectedMoreThanOnce . '" />';
$html[] = '<div class="form-multigroup-wrap t3js-formengine-field-group">';
$html[] = '<div class="form-multigroup-item">';
$html[] = '<label>';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<select';
$html[] = ' id="' . $selectedOptionsFieldId . '"';
$html[] = ' size="' . $size . '"';
$html[] = ' class="form-select"';
$html[] = $multipleAttribute;
$html[] = ' data-formengine-input-name="' . htmlspecialchars($elementName) . '"';
$html[] = '>';
$html[] = implode(LF, $selectedItemsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--move">';
$html[] = '<div class="btn-group-vertical">';
if ($maxItems > 1 && $size >= 2) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-top"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-top', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_top')) . '</span>';
$html[] = '</button>';
}
if ($maxItems > 1) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-up"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-up', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_up')) . '</span>';
$html[] = '</button>';
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-down"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-down', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_down')) . '</span>';
$html[] = '</button>';
}
if ($maxItems > 1 && $size >= 2) {
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-moveoption-bottom"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-move-to-bottom', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move_to_bottom')) . '</span>';
$html[] = '</button>';
}
$html[] = $alternativeFieldControlHtml;
$html[] = '<button type="button"';
$html[] = ' class="btn btn-default t3js-btn-option t3js-btn-removeoption"';
$html[] = ' data-fieldname="' . htmlspecialchars($elementName) . '"';
$html[] = ' title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '"';
$html[] = '>';
$html[] = $this->iconFactory->getIcon('actions-selection-delete', IconSize::SMALL)->render();
$html[] = '<span class="visually-hidden">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.remove_selected')) . '</span>';
$html[] = '</button>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<div class="form-multigroup-item">';
$html[] = '<label for="' . $filterTextfieldId . '">';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.items'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap">';
$html[] = implode(LF, $filterHtml);
$html[] = '<div class="form-wizards-item-element">';
$selectElementAttrs = array_merge(
[
'size' => $size,
'id' => $availableOptionsFieldId,
'class' => 'form-select t3js-formengine-select-itemstoselect',
'data-relatedfieldname' => $elementName,
'data-exclusivevalues' => $config['exclusiveKeys'] ?? '',
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
$html[] = '<select ' . GeneralUtility::implodeAttributes($selectElementAttrs, true) . '>';
$html[] = implode(LF, $selectableItemsHtml);
$html[] = '</select>';
$html[] = '</div>';
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = '<div class="btn-group-vertical">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<input type="hidden" name="' . htmlspecialchars($elementName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
$html[] = '</div>';
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/select-multiple-side-by-side-element.js'
)->instance($selectedOptionsFieldId, $availableOptionsFieldId);
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* Create HTML of a read only multi select. Right side is not
* rendered, but just the left side with the selected items.
*
* @return array
*/
protected function renderReadOnly()
{
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$fieldName = $parameterArray['itemFormElName'];
$possibleItems = $config['items'];
$selectedItems = $parameterArray['itemFormElValue'] ?: [];
if (!is_array($selectedItems)) {
$selectedItems = GeneralUtility::trimExplode(',', $selectedItems, true);
}
$size = (int)($config['size'] ?? 2);
$autoSizeMax = (int)($config['autoSizeMax'] ?? 0);
if ($autoSizeMax > 0) {
$size = MathUtility::forceIntegerInRange($size, 1);
$size = MathUtility::forceIntegerInRange(count($selectedItems) + 1, $size, $autoSizeMax);
}
$multiple = '';
if ($size !== 1) {
$multiple = ' multiple="multiple"';
}
$listOfSelectedValues = [];
$optionsHtml = [];
foreach ($selectedItems as $itemValue) {
foreach ($possibleItems as $possibleItem) {
if ($possibleItem['value'] == $itemValue) {
$title = $possibleItem['label'];
$listOfSelectedValues[] = $itemValue;
$optionsHtml[] = '<option value="' . htmlspecialchars($itemValue) . '" title="' . htmlspecialchars($title) . '">' . htmlspecialchars($title) . '</option>';
break;
}
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$selectId = StringUtility::getUniqueId('tceforms-multiselect-');
$html = [];
$html[] = $this->renderLabel($selectId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<label>';
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.selected'));
$html[] = '</label>';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<select';
$html[] = ' id="' . $selectId . '"';
$html[] = ' size="' . $size . '"';
$html[] = ' class="form-select"';
$html[] = $multiple;
$html[] = ' data-formengine-input-name="' . htmlspecialchars($fieldName) . '"';
$html[] = ' disabled="disabled">';
$html[] = '/>';
$html[] = implode(LF, $optionsHtml);
$html[] = '</select>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '<input type="hidden" name="' . htmlspecialchars($fieldName) . '" value="' . htmlspecialchars(implode(',', $listOfSelectedValues)) . '" />';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
protected function getBackendUserAuthentication(): BackendUserAuthentication
{
return $GLOBALS['BE_USER'];
}
}
@@ -0,0 +1,210 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Create a widget with a select box where multiple items can be selected
*
* This is rendered for config type=select, renderType=selectSingleBox
*/
class SelectSingleBoxElement extends AbstractFormElement
{
/**
* Default field controls for this element.
*
* @var array
*/
protected $defaultFieldControl = [
'resetSelection' => [
'renderType' => 'resetSelection',
],
];
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* This will render a selector box element, or possibly a special construction with two selector boxes.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$languageService = $this->getLanguageService();
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
// Field configuration from TCA:
$config = $parameterArray['fieldConf']['config'];
$selectItems = $parameterArray['fieldConf']['config']['items'];
$disabled = !empty($config['readOnly']);
// Get item value as array and make unique, which is fine because there can be no duplicates anyway.
$itemArray = array_flip($parameterArray['itemFormElValue']);
$width = $this->formMaxWidth($this->defaultInputWidth);
$optionElements = [];
foreach ($selectItems as $item) {
$value = $item['value'];
$attributes = [];
// Selected or not by default
if (isset($itemArray[$value])) {
$attributes['selected'] = 'selected';
unset($itemArray[$value]);
}
// Non-selectable element
if ((string)$value === '--div--') {
$attributes['disabled'] = 'disabled';
$attributes['class'] = 'formcontrol-select-divider';
}
$optionElements[] = $this->renderOptionElement($value, $item['label'], $attributes);
}
$selectItems = $parameterArray['fieldConf']['config']['items'];
$size = (int)($config['size'] ?? 0);
$autoSizeMax = (int)($config['autoSizeMax'] ?? 0);
if ($autoSizeMax > 0) {
$size = MathUtility::forceIntegerInRange($size, 1);
$size = MathUtility::forceIntegerInRange(count($selectItems) + 1, $size, $autoSizeMax);
}
$selectId = StringUtility::getUniqueId($size === 1 ? 'tceforms-select' : 'tceforms-multiselect');
$selectElement = $this->renderSelectElement($optionElements, $parameterArray, $config, $selectId, $size);
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = $this->renderLabel($selectId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
if (!$disabled) {
// Add an empty hidden field which will send a blank value if all items are unselected.
$html[] = '<input type="hidden" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="">';
}
$html[] = $selectElement;
$html[] = '</div>';
if (!$disabled) {
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '<div class="form-text">';
$html[] = '<em>' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.holdDownCTRL')) . '</em>';
$html[] = '</div>';
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
} else {
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
/**
* Renders a <select> element
*/
protected function renderSelectElement(array $optionElements, array $parameterArray, array $config, string $selectId, int $size): string
{
$attributes = array_merge(
[
'name' => $parameterArray['itemFormElName'] . '[]',
'multiple' => 'multiple',
'id' => $selectId,
'class' => 'form-select ',
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
if ($size) {
$attributes['size'] = (string)$size;
}
if ($config['readOnly'] ?? false) {
$attributes['disabled'] = 'disabled';
}
$html = [];
$html[] = '<select ' . GeneralUtility::implodeAttributes($attributes, true) . '>';
$html[] = implode(LF, $optionElements);
$html[] = '</select>';
return implode(LF, $html);
}
/**
* Renders a single <option> element
*
* @param string $value The option value
* @param string $label The option label
* @param array $attributes Map of attribute names and values
* @return string
*/
protected function renderOptionElement($value, $label, array $attributes = [])
{
$attributes['value'] = $value;
$html = [
'<option ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>',
htmlspecialchars($this->appendValueToLabelInDebugMode($label, $value), ENT_COMPAT, 'UTF-8', false),
'</option>',
];
return implode('', $html);
}
}
@@ -0,0 +1,261 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Creates a widget where only one item can be selected.
* This is either a select drop-down if no size config is given or set to 1, or a select box.
*
* This is rendered for type=select, renderType=selectSingle
*/
class SelectSingleElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'selectIcons' => [
'renderType' => 'selectIcons',
'disabled' => true,
],
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
'after' => [
'selectIcons',
],
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [ 'localizationStateSelector' ],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [ 'otherLanguageContent' ],
],
];
public function __construct(
private readonly InlineStackProcessor $inlineStackProcessor,
) {}
/**
* Render single element
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$table = $this->data['tableName'];
$field = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$selectItems = $parameterArray['fieldConf']['config']['items'];
$classList = ['form-select', 'form-control-adapt'];
// Check against inline uniqueness
$uniqueIds = [];
if (($this->data['isInlineChild'] ?? false) && ($this->data['inlineParentUid'] ?? false)) {
// If config[foreign_unique] is set for the parent inline field, all
// already used unique ids must be excluded from the select items.
$inlineObjectName = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid']);
if (($this->data['inlineParentConfig']['foreign_table'] ?? false) === $table
&& ($this->data['inlineParentConfig']['foreign_unique'] ?? false) === $field
) {
$classList[] = 't3js-inline-unique';
$uniqueIds = $this->data['inlineData']['unique'][$inlineObjectName . '-' . $table]['used'] ?? [];
}
// hide uid of parent record for symmetric relations
if (($this->data['inlineParentConfig']['foreign_table'] ?? false) === $table
&& (
($this->data['inlineParentConfig']['foreign_field'] ?? false) === $field
|| ($this->data['inlineParentConfig']['symmetric_field'] ?? false) === $field
)
) {
$uniqueIds[] = $this->data['inlineParentUid'];
}
$uniqueIds = array_map(intval(...), $uniqueIds);
}
// Initialization:
$selectId = StringUtility::getUniqueId('tceforms-select-');
$selectedItem = null;
$size = (int)($config['size'] ?? 0);
// Style set on <select/>
$options = '';
$disabled = false;
if (!empty($config['readOnly'])) {
$disabled = true;
}
// Prepare groups
$selectItemCounter = 0;
$selectItemGroupCount = 0;
$selectItemGroups = [];
$selectedValue = '';
$hasIcons = false;
// In case e.g. "l10n_display" is set to "defaultAsReadonly" only one value (as string) could be handed in
if (!empty($parameterArray['itemFormElValue'])) {
if (is_array($parameterArray['itemFormElValue'])) {
$selectedValue = (string)$parameterArray['itemFormElValue'][0];
} else {
$selectedValue = (string)$parameterArray['itemFormElValue'];
}
}
foreach ($selectItems as $item) {
$selected = $selectedValue === (string)$item['value'];
if ($item['value'] === '--div--') {
// IS OPTGROUP
if ($selectItemCounter !== 0) {
$selectItemGroupCount++;
}
$selectItemGroups[$selectItemGroupCount]['header'] = [
'title' => $item['label'],
];
} elseif ($selected || !in_array((int)$item['value'], $uniqueIds, true)) {
$icon = !empty($item['icon']) ? FormEngineUtility::getIconHtml($item['icon'], $item['label'], $item['label']) : '';
$enhancedItem = [
'title' => $this->appendValueToLabelInDebugMode($item['label'], $item['value']),
'value' => $item['value'],
'icon' => $icon,
'selected' => $selected,
];
if ($selected) {
$selectedItem = $enhancedItem;
}
$selectItemGroups[$selectItemGroupCount]['items'][] = $enhancedItem;
$selectItemCounter++;
}
}
// Process groups
foreach ($selectItemGroups as $selectItemGroup) {
// suppress groups without items
if (empty($selectItemGroup['items'])) {
continue;
}
$optionGroup = is_array($selectItemGroup['header'] ?? null);
$options .= ($optionGroup ? '<optgroup label="' . htmlspecialchars($selectItemGroup['header']['title'], ENT_COMPAT, 'UTF-8', false) . '">' : '');
foreach ($selectItemGroup['items'] as $item) {
$options .= '<option value="' . htmlspecialchars($item['value']) . '" data-icon="'
. htmlspecialchars($item['icon']) . '"'
. ($item['selected'] ? ' selected="selected"' : '') . '>' . htmlspecialchars($item['title'], ENT_COMPAT, 'UTF-8', false) . '</option>';
// At least one select item with icon found.
if (!empty($item['icon'])) {
$hasIcons = true;
}
}
$options .= ($optionGroup ? '</optgroup>' : '');
}
// No item selected. Use first item of first group as selected item, which is display
// in the form to render icon of that item icon as selected icon when item has one.
if ($hasIcons
&& $selectedItem === null
&& isset($selectItemGroups[0]['items'][0])
) {
$selectedItem = $selectItemGroups[0]['items'][0];
}
$selectAttributes = [
'id' => $selectId,
'name' => (string)($parameterArray['itemFormElName'] ?? ''),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'class' => implode(' ', $classList),
];
if ($size) {
$selectAttributes['size'] = (string)$size;
}
if ($disabled) {
$selectAttributes['disabled'] = 'disabled';
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = $this->renderLabel($selectId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
if ($hasIcons) {
$html[] = '<div class="input-group">';
$html[] = '<span class="input-group-text input-group-icon">';
if ($selectedItem !== null) {
$html[] = $selectedItem['icon'];
}
$html[] = '</span>';
}
$html[] = '<select ' . GeneralUtility::implodeAttributes($selectAttributes, true) . '>';
$html[] = $options;
$html[] = '</select>';
if ($hasIcons) {
$html[] = '</div>';
}
$html[] = '</div>';
if (!$disabled && !empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = '<div class="btn-group">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
if (!$disabled && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$onFieldChangeItems = $this->getOnFieldChangeItems($parameterArray['fieldChangeFunc'] ?? []);
$resultArray['javaScriptModules']['selectSingleElement'] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/select-single-element.js'
)->invoke('initializeOnReady', '#' . $selectId, ['onChange' => $onFieldChangeItems]);
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
}
+201
View File
@@ -0,0 +1,201 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Form\Behavior\OnFieldChangeInterface;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Render data as a tree.
*
* Typically rendered for config type=select, renderType=selectTree
*/
class SelectTreeElement extends AbstractFormElement
{
/**
* @var array Default wizards
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
];
/**
* Default number of tree nodes to show (determines tree height)
* when no ['config']['size'] is set
*
* @var int
*/
protected $itemsToShow = 15;
/**
* Number of items to show at last
* e.g. when you have only 2 items in a tree
*
* @var int
*/
protected $minItemsToShow = 5;
/**
* Pixel height of a single tree node
*
* @var int
*/
protected $itemHeight = 20;
/**
* Render tree widget
*
* @return array As defined in initializeResultArray() of AbstractNode
* @see AbstractNode::initializeResultArray()
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$formElementId = md5($parameterArray['itemFormElName']);
// Field configuration from TCA:
$config = $parameterArray['fieldConf']['config'];
$readOnly = !empty($config['readOnly']);
$exclusiveKeys = !empty($config['exclusiveKeys']) ? $config['exclusiveKeys'] : '';
$exclusiveKeys = $exclusiveKeys . ',';
$appearance = !empty($config['treeConfig']['appearance']) ? $config['treeConfig']['appearance'] : [];
$expanded = !empty($appearance['expandAll']);
$showHeader = !empty($appearance['showHeader']);
if (isset($config['size']) && (int)$config['size'] > 0) {
$height = max($this->minItemsToShow, (int)$config['size']);
} else {
$height = $this->itemsToShow;
}
$heightInPx = $height * $this->itemHeight;
$treeWrapperId = 'tree_' . $formElementId;
$fieldId = 'tree_record_' . $formElementId;
$fieldName = $this->data['fieldName'];
$dataStructureIdentifier = '';
$flexFormSheetName = '';
$flexFormFieldName = '';
$flexFormContainerName = '';
$flexFormContainerIdentifier = '';
$flexFormContainerFieldName = '';
$flexFormSectionContainerIsNew = false;
if ($this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
$dataStructureIdentifier = $this->data['processedTca']['columns'][$fieldName]['config']['dataStructureIdentifier'];
if (isset($this->data['flexFormSheetName'])) {
$flexFormSheetName = $this->data['flexFormSheetName'];
}
if (isset($this->data['flexFormFieldName'])) {
$flexFormFieldName = $this->data['flexFormFieldName'];
}
if (isset($this->data['flexFormContainerName'])) {
$flexFormContainerName = $this->data['flexFormContainerName'];
}
if (isset($this->data['flexFormContainerFieldName'])) {
$flexFormContainerFieldName = $this->data['flexFormContainerFieldName'];
}
if (isset($this->data['flexFormContainerIdentifier'])) {
$flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier'];
}
// Add a flag this is a tree in a new flex section container element. This is needed to initialize
// the databaseRow with this container again so the tree data provider is able to calculate tree items.
if (!empty($this->data['flexSectionContainerPreparation'])) {
$flexFormSectionContainerIsNew = true;
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="typo3-tceforms-tree">';
$html[] = '<input class="treeRecord" type="hidden" id="' . htmlspecialchars($fieldId) . '"';
$html[] = ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '"';
$html[] = ' data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$html[] = ' data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
$html[] = ' data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
$html[] = ' data-uid="' . (int)$this->data['vanillaUid'] . '"';
$html[] = ' data-recordtypevalue="' . htmlspecialchars($this->data['recordTypeValue']) . '"';
$html[] = ' data-datastructureidentifier="' . htmlspecialchars($dataStructureIdentifier) . '"';
$html[] = ' data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
$html[] = ' data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
$html[] = ' data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
$html[] = ' data-flexformcontaineridentifier="' . htmlspecialchars($flexFormContainerIdentifier) . '"';
$html[] = ' data-flexformcontainerfieldname="' . htmlspecialchars($flexFormContainerFieldName) . '"';
$html[] = ' data-flexformsectioncontainerisnew="' . htmlspecialchars((string)$flexFormSectionContainerIsNew) . '"';
$html[] = ' data-command="' . htmlspecialchars($this->data['command']) . '"';
$html[] = ' data-read-only="' . ($readOnly ? '1' : '0') . '"';
$html[] = ' data-tree-exclusive-keys="' . htmlspecialchars($exclusiveKeys) . '"';
$html[] = ' data-tree-expand-up-to-level="' . ($expanded ? '999' : '1') . '"';
$html[] = ' data-tree-show-toolbar="' . $showHeader . '"';
$html[] = ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$html[] = ' id="treeinput' . $formElementId . '"';
$html[] = ' value="' . htmlspecialchars(implode(',', $parameterArray['itemFormElValue'])) . '"';
$html[] = ' data-overridevalues="' . GeneralUtility::jsonEncodeForHtmlAttribute($this->data['overrideValues']) . '"';
$html[] = ' data-defaultvalues="' . GeneralUtility::jsonEncodeForHtmlAttribute($this->data['defaultValues']) . '"';
$html[] = '/>';
$html[] = '</div>';
$html[] = '<div id="' . $treeWrapperId . '" class="tree-element" style="height: ' . $heightInPx . 'px;"></div>';
$html[] = '</div>';
if (!$readOnly && !empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
$onFieldChangeItems = $this->getOnFieldChangeItems($this->getFieldChangeFuncs());
$resultArray['javaScriptModules']['selectTreeElement'] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/select-tree-element.js',
'SelectTreeElement'
)->instance($treeWrapperId, $fieldId, null, $onFieldChangeItems);
return $resultArray;
}
/**
* @return list<OnFieldChangeInterface>
*/
protected function getFieldChangeFuncs(): array
{
$items = [];
$parameterArray = $this->data['parameterArray'];
if (!empty($parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'])) {
$items[] = $parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'];
}
if (!empty($parameterArray['fieldChangeFunc']['alert'])) {
$items[] = $parameterArray['fieldChangeFunc']['alert'];
}
return $items;
}
}
@@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Renders table permission options for each available table.
*
* @internal Only used in be_groups for the "combined read & write" table permission list.
*/
final class TablePermissionElement extends AbstractFormElement
{
private const array Permissions = [
'none' => 'none',
'select' => 'select',
'modify' => 'modify',
];
public function __construct(
private readonly IconFactory $iconFactory,
) {}
public function render(): array
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$config = $parameterArray['fieldConf']['config'];
$elementFieldName = $parameterArray['itemFormElName'];
$currentValue = ['modify' => [], 'select' => []];
if (is_array($parameterArray['itemFormElValue']['modify'] ?? false)
&& is_array($parameterArray['itemFormElValue']['select'] ?? false)
) {
$currentValue = $parameterArray['itemFormElValue'];
}
$readOnly = (bool)($config['readOnly'] ?? false);
$availableTables = $config['items'] ?? [];
if (empty($availableTables)) {
// Early return in case the field does not contain any items
return $resultArray;
}
$tablesConfiguration = [];
$lang = $this->getLanguageService();
$itemArrayModify = array_flip($currentValue['modify']);
$itemArraySelect = array_flip($currentValue['select']);
$elementId = StringUtility::getUniqueId('formengine-table-permission-');
foreach ($availableTables as $table) {
$permissions = [];
foreach (self::Permissions as $permission) {
$permissions[$permission] = [
'label' => $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tables_modify.permissions.' . $permission),
'attributes' => [
'type' => 'radio',
'class' => 'form-check-input t3js-table-permissions-item t3js-multi-record-selection-check',
'value' => $permission,
'name' => $elementId . '[' . $table['value'] . ']',
'id' => $elementId . '[' . $table['value'] . '][' . $permission . ']',
'data-table' => $table['value'],
],
];
}
if (isset($itemArrayModify[$table['value']])) {
$permissions[self::Permissions['modify']]['attributes']['checked'] = 'checked';
} elseif (isset($itemArraySelect[$table['value']])) {
$permissions[self::Permissions['select']]['attributes']['checked'] = 'checked';
} else {
$permissions[self::Permissions['none']]['attributes']['checked'] = 'checked';
}
if ($readOnly) {
foreach (self::Permissions as $permission) {
$permissions[$permission]['attributes']['disabled'] = 'disabled';
}
}
$tablesConfiguration[] = [
'permissions' => $permissions,
'label' => [
'id' => $elementId . '-' . $table['value'] . '-label',
'icon' => $this->getIconForTable(!empty($table['icon']) ? $table['icon'] : 'empty-empty'),
'title' => $lang->sL($table['label']),
'value' => $table['value'],
],
];
}
$modifyStateFieldName = htmlspecialchars($elementFieldName);
$selectStateFieldName = htmlspecialchars(str_replace($this->data['fieldName'], $config['selectFieldName'], $elementFieldName));
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$html[] = '<typo3-formengine-element-tablepermission modifyStateFieldName="' . $modifyStateFieldName . '" selectStateFieldName="' . $selectStateFieldName . '">';
$html[] = '<div class="formengine-field-item t3js-formengine-field-item" data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString($config)) . '">';
$html[] = $fieldInformationHtml;
if (!$readOnly) {
$html[] = '<input type="hidden" name="' . $modifyStateFieldName . '" value="' . implode(',', $currentValue['modify']) . '">';
$html[] = '<input type="hidden" name="' . $selectStateFieldName . '" value="' . implode(',', $currentValue['select']) . '">';
}
$tableRows = [];
foreach ($tablesConfiguration as $tableConfiguration) {
$tableRows[] = '<tr role="radiogroup" aria-labelledby="' . $tableConfiguration['label']['id'] . '">';
foreach ($tableConfiguration['permissions'] as $key => $permission) {
$tableRows[] = '<td class="col-radiogroup">';
$tableRows[] = '<div class="form-check form-check-inline" data-multi-record-selection-identifier="' . $elementId . '-' . $key . '" data-multi-record-selection-element="true">';
$tableRows[] = '<input ' . GeneralUtility::implodeAttributes($permission['attributes'], true, true) . '>';
$tableRows[] = '<label class="form-check-label" for="' . $permission['attributes']['id'] . '">' . htmlspecialchars($permission['label']) . '</label>';
$tableRows[] = '</div>';
$tableRows[] = '</td>';
}
$tableRows[] = '<td class="col-title col-border-left">';
$tableRows[] = '<label class="label-block nowrap-disabled" id="' . $tableConfiguration['label']['id'] . '">';
$tableRows[] = '<span>' . $tableConfiguration['label']['icon'] . '</span>';
$tableRows[] = htmlspecialchars($this->appendValueToLabelInDebugMode($tableConfiguration['label']['title'], $tableConfiguration['label']['value']));
$tableRows[] = '</label>';
$tableRows[] = '</td>';
$tableRows[] = '</tr>';
}
$html[] = '<div id="' . $elementId . '">';
$html[] = '<div class="table-fit">';
$html[] = '<table class="table table-hover">';
$html[] = '<thead>';
$html[] = '<tr>';
foreach (self::Permissions as $permission) {
$html[] = '<th data-multi-record-selection-identifier="' . $elementId . '-' . $permission . '">';
$html[] = $this->getRecordSelectionCheckActions($permission === self::Permissions['none'] ? ['all'] : ['all', 'none', 'toggle'], $readOnly);
$html[] = '</th>';
}
$html[] = '<th class="col-title col-border-left">' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.th.name')) . '</th>';
$html[] = '</tr>';
$html[] = '</thead>';
$html[] = '<tbody>' . implode(LF, $tableRows) . '</tbody>';
$html[] = '</table>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
if (!$readOnly) {
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/table-permission-element.js');
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/multi-record-selection.js');
}
$html[] = '</typo3-formengine-element-tablepermission>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
protected function wrapWithFieldsetAndLegend(string $innerHTML): string
{
$legend = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:be_groups.tables_modify'));
if ($this->getBackendUser()->shallDisplayDebugInformation()) {
$legend .= ' <code>[' . ($this->data['parameterArray']['fieldConf']['config']['selectFieldName'] ?? '') . ', ' . $this->data['fieldName'] . ']</code>';
}
$html = [];
$html[] = '<fieldset>';
$html[] = '<legend class="form-label t3js-formengine-label">' . $legend . '</legend>';
$html[] = $innerHTML;
$html[] = '</fieldset>';
return implode(LF, $html);
}
private function getIconForTable(string $icon): string
{
return FormEngineUtility::getIconHtml($icon);
}
private function getRecordSelectionCheckActions(array $optionsToShow, bool $readOnly): string
{
$checkboxOptions = [
'all' => '
<li>
<button type="button" class="dropdown-item" disabled data-multi-record-selection-check-action="check-all" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll') . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-all', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.checkAll') . '
</span>
</span>
</button>
</li>',
'none' => '
<li>
<button type="button" class="dropdown-item" disabled data-multi-record-selection-check-action="check-none" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll') . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-none', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.uncheckAll') . '
</span>
</span>
</button>
</li>',
'toggle' => '
<li>
<button type="button" class="dropdown-item" data-multi-record-selection-check-action="toggle" title="' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection') . '">
<span class="dropdown-item-columns">
<span class="dropdown-item-column dropdown-item-column-icon" aria-hidden="true">
' . $this->iconFactory->getIcon('actions-selection-elements-invert', IconSize::SMALL)->render() . '
</span>
<span class="dropdown-item-column dropdown-item-column-title">
' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleSelection') . '
</span>
</span>
</button>
</li>',
];
$checkboxOptions = array_filter($checkboxOptions, static fn($checkboxOption) => in_array($checkboxOption, $optionsToShow, true), ARRAY_FILTER_USE_KEY);
if ($checkboxOptions === []) {
return '';
}
return '
<div class="dropdown">
<button type="button" class="dropdown-toggle dropdown-toggle-link t3js-multi-record-selection-check-actions-toggle" data-bs-toggle="dropdown" data-bs-boundary="window" aria-expanded="false" ' . ($readOnly ? ' disabled="disabled"' : '') . '>
<core:icon identifier="actions-selection" size="small" />
' . $this->iconFactory->getIcon('actions-selection', IconSize::SMALL)->render() . '
</button>
<ul class="dropdown-menu t3js-multi-record-selection-check-actions">
' . implode(LF, $checkboxOptions) . '
</ul>
</div>';
}
}
+310
View File
@@ -0,0 +1,310 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* General type=text element
*
* The InputTextElement renders a html textarea field.
*/
class TextElement extends AbstractFormElement
{
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* The number of chars expected per row when the height of a text area field is
* automatically calculated based on the number of characters found in the field content.
*
* @var int
*/
protected $charactersPerRow = 40;
/**
* This will render a <textarea>
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$table = $this->data['tableName'];
$fieldName = $this->data['fieldName'];
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$itemValue = $parameterArray['itemFormElValue'];
$config = $parameterArray['fieldConf']['config'];
$width = null;
if ($config['cols'] ?? false) {
$width = $this->formMaxWidth(MathUtility::forceIntegerInRange($config['cols'], $this->minimumInputWidth, $this->maxInputWidth));
}
$fieldId = StringUtility::getUniqueId('formengine-textarea-');
$itemName = (string)$parameterArray['itemFormElName'];
$renderedLabel = $this->renderLabel($fieldId);
// Setting number of rows
$rows = MathUtility::forceIntegerInRange(($config['rows'] ?? 5) ?: 5, 1, 20);
$originalRows = $rows;
$itemFormElementValueLength = strlen((string)$itemValue);
if ($itemFormElementValueLength > ($this->charactersPerRow * 2)) {
$rows = MathUtility::forceIntegerInRange(
(int)round($itemFormElementValueLength / $this->charactersPerRow),
count(explode(LF, (string)$itemValue)),
20
);
if ($rows < $originalRows) {
$rows = $originalRows;
}
}
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
$html = [];
$html[] = $renderedLabel;
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px">' : '>');
$html[] = GeneralUtility::renderTextarea((string)$itemValue, ['class' => 'form-control', 'id' => $fieldId, 'name' => $itemName, 'rows' => $rows, 'disabled' => 'disabled']);
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
$languageService = $this->getLanguageService();
// @todo: The whole eval handling is a mess and needs refactoring - Especially for this element,
// since the resolved $evalList is currently not used at all, because FormEngineValidation
// does not support eval for <textarea> elements.
$evalList = GeneralUtility::trimExplode(',', $config['eval'] ?? '', true);
foreach ($evalList as $func) {
// @todo: This is ugly: The code should find out on it's own whether an eval definition is a
// @todo: keyword like "date", or a class reference. The global registration could be dropped then
// Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
// There is a similar hook for "evaluateFieldValue" in DataHandler and InputTextElement
if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
if (class_exists($func)) {
$evalObj = GeneralUtility::makeInstance($func);
if (method_exists($evalObj, 'deevaluateFieldValue')) {
$_params = [
'value' => $itemValue,
];
$itemValue = $evalObj->deevaluateFieldValue($_params);
}
}
}
}
$attributes = array_merge(
[
'id' => $fieldId,
'name' => $itemName,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-name' => $itemName,
'rows' => (string)$rows,
'wrap' => (string)(($config['wrap'] ?? 'virtual') ?: 'virtual'),
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
$classes = [
'form-control',
't3js-formengine-textarea',
'formengine-textarea',
];
if ($config['fixedFont'] ?? false) {
$classes[] = 'font-monospace';
}
if ($config['enableTabulator'] ?? false) {
$classes[] = 't3js-enable-tab';
}
$attributes['class'] = implode(' ', $classes);
$maxLength = (int)($config['max'] ?? 0);
if ($maxLength > 0) {
$attributes['maxlength'] = (string)$maxLength;
}
$minlength = (int)($config['min'] ?? 0);
if ($minlength > 0 && ($maxLength === 0 || $minlength <= $maxLength)) {
$attributes['minlength'] = (string)$minlength;
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = trim($config['placeholder']);
}
$valuePickerHtml = [];
if (is_array($config['valuePicker']['items'] ?? false)) {
$valuePickerConfiguration = [
'linked-field' => '[data-formengine-input-name="' . $itemName . '"]',
];
$valuePickerAttributes = array_merge(
[
'class' => 'form-select form-control-adapt',
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
$valuePickerHtml[] = '<typo3-formengine-valuepicker ' . GeneralUtility::implodeAttributes($valuePickerConfiguration, true) . '>';
$valuePickerHtml[] = '<select ' . GeneralUtility::implodeAttributes($valuePickerAttributes, true) . '>';
$valuePickerHtml[] = '<option></option>';
foreach ($config['valuePicker']['items'] as $item) {
$valuePickerHtml[] = '<option value="' . htmlspecialchars($item['value']) . '">' . htmlspecialchars($languageService->sL($item['label'])) . '</option>';
}
$valuePickerHtml[] = '</select>';
$valuePickerHtml[] = '</typo3-formengine-valuepicker>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-wizard/value-picker.js');
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$mainFieldHtml = [];
$mainFieldHtml[] = '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px">' : '>');
$mainFieldHtml[] = '<div class="form-wizards-wrap">';
$mainFieldHtml[] = '<div class="form-wizards-item-element">';
$mainFieldHtml[] = GeneralUtility::renderTextarea((string)$itemValue, $attributes);
$mainFieldHtml[] = '</div>';
if (!empty($valuePickerHtml) || !empty($fieldControlHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$mainFieldHtml[] = '<div class="btn-group">';
$mainFieldHtml[] = implode(LF, $valuePickerHtml);
$mainFieldHtml[] = $fieldControlHtml;
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$mainFieldHtml[] = '<div class="form-wizards-item-bottom">';
$mainFieldHtml[] = $fieldWizardHtml;
$mainFieldHtml[] = '</div>';
}
$mainFieldHtml[] = '</div>';
$mainFieldHtml[] = '</div>';
$mainFieldHtml = implode(LF, $mainFieldHtml);
$nullControlNameEscaped = htmlspecialchars('control[active][' . $table . '][' . $this->data['databaseRow']['uid'] . '][' . $fieldName . ']');
$fullElement = $mainFieldHtml;
if ($this->hasNullCheckboxButNoPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$fullElement = [];
$fullElement[] = '<div class="t3-form-field-disable"></div>';
$fullElement[] = '<div class="form-check t3-form-field-eval-null-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.nullCheckbox');
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = $mainFieldHtml;
$fullElement = implode(LF, $fullElement);
} elseif ($this->hasNullCheckboxWithPlaceholder()) {
$checked = $itemValue !== null ? ' checked="checked"' : '';
$placeholder = $shortenedPlaceholder = (string)($config['placeholder'] ?? '');
if ($placeholder !== '') {
$shortenedPlaceholder = GeneralUtility::fixed_lgd_cs($placeholder, 20);
if ($placeholder !== $shortenedPlaceholder) {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
'<span title="' . htmlspecialchars($placeholder) . '">' . htmlspecialchars($shortenedPlaceholder) . '</span>'
);
} else {
$overrideLabel = sprintf(
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override'),
htmlspecialchars($placeholder)
);
}
} else {
$overrideLabel = $languageService->sL(
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.placeholder.override_not_available'
);
}
$textareaAttributes = [
'class' => 'form-control formengine-textarea' . (isset($config['fixedFont']) ? ' font-monospace' : ''),
'disabled' => 'disabled',
'rows' => $attributes['rows'],
'wrap' => $attributes['wrap'],
...(isset($attributes['style']) ? ['style' => $attributes['style']] : []),
...(isset($attributes['maxlength']) ? ['maxlength' => $attributes['maxlength']] : []),
];
$fullElement = [];
$fullElement[] = '<div class="form-check t3js-form-field-eval-null-placeholder-checkbox">';
$fullElement[] = '<input type="hidden" name="' . $nullControlNameEscaped . '" value="0" />';
$fullElement[] = '<input type="checkbox" class="form-check-input" name="' . $nullControlNameEscaped . '" id="' . $nullControlNameEscaped . '" value="1"' . $checked . ' />';
$fullElement[] = '<label class="form-check-label" for="' . $nullControlNameEscaped . '">';
$fullElement[] = $overrideLabel;
$fullElement[] = '</label>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-placeholder">';
$fullElement[] = '<div class="form-control-wrap"' . ($width ? ' style="max-width: ' . $width . 'px">' : '>');
$fullElement[] = GeneralUtility::renderTextarea($shortenedPlaceholder, $textareaAttributes);
$fullElement[] = '</div>';
$fullElement[] = '</div>';
$fullElement[] = '<div class="t3js-formengine-placeholder-formfield">';
$fullElement[] = $mainFieldHtml;
$fullElement[] = '</div>';
$fullElement = implode(LF, $fullElement);
}
$resultArray['html'] = $renderedLabel . '
<typo3-formengine-element-text class="formengine-field-item t3js-formengine-field-item" recordFieldId="' . htmlspecialchars($fieldId) . '">
' . $fieldInformationHtml . '
' . $fullElement . '
</typo3-formengine-element-text>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/text-element.js');
return $resultArray;
}
}
+236
View File
@@ -0,0 +1,236 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render the table editor
* @internal This class is a TYPO3 Backend implementation and is not considered part of the Public TYPO3 API.
*/
class TextTableElement extends AbstractFormElement
{
/**
* Number of new rows to add in bottom of wizard
*/
protected int $numNewRows = 1;
/**
* Default field wizards enabled for this element.
*
* @var array
*/
protected $defaultFieldWizard = [
'localizationStateSelector' => [
'renderType' => 'localizationStateSelector',
],
'otherLanguageContent' => [
'renderType' => 'otherLanguageContent',
'after' => [
'localizationStateSelector',
],
],
'defaultLanguageDifferences' => [
'renderType' => 'defaultLanguageDifferences',
'after' => [
'otherLanguageContent',
],
],
];
/**
* The number of chars expected per row when the height of a text area field is
* automatically calculated based on the number of characters found in the field content.
*
* @var int
*/
protected $charactersPerRow = 40;
/**
* This will render a <textarea> with table wizard
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$parameterArray = $this->data['parameterArray'];
$resultArray = $this->initializeResultArray();
$itemValue = $parameterArray['itemFormElValue'];
$config = $parameterArray['fieldConf']['config'];
$evalList = GeneralUtility::trimExplode(',', $config['eval'] ?? '', true);
// Setting number of rows
$rows = $config['rows'] ?? 0;
$rows = MathUtility::forceIntegerInRange($rows ?: 5, 1, 20);
$originalRows = $rows;
$itemFormElementValueLength = strlen($itemValue);
if ($itemFormElementValueLength > $this->charactersPerRow * 2) {
$rows = MathUtility::forceIntegerInRange(
(int)round($itemFormElementValueLength / $this->charactersPerRow),
count(explode(LF, $itemValue)),
20
);
if ($rows < $originalRows) {
$rows = $originalRows;
}
}
$fieldId = StringUtility::getUniqueId('formengine-textarea-');
$itemName = (string)$parameterArray['itemFormElName'];
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if ($config['readOnly'] ?? false) {
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-control-wrap" style="overflow: auto;">';
$html[] = GeneralUtility::renderTextarea($itemValue, [ 'class' => 'form-control', 'id' => $fieldId, 'name' => $itemName, 'rows' => $rows, 'disabled' => 'disabled']);
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
// @todo: The whole eval handling is a mess and needs refactoring
foreach ($evalList as $func) {
// @todo: This is ugly: The code should find out on it's own whether an eval definition is a
// @todo: keyword like "date", or a class reference. The global registration could be dropped then
// Pair hook to the one in \TYPO3\CMS\Core\DataHandling\DataHandler::checkValue_input_Eval()
// There is a similar hook for "evaluateFieldValue" in DataHandler and InputTextElement
if (isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tce']['formevals'][$func])) {
if (class_exists($func)) {
$evalObj = GeneralUtility::makeInstance($func);
if (method_exists($evalObj, 'deevaluateFieldValue')) {
$_params = [
'value' => $itemValue,
];
$itemValue = $evalObj->deevaluateFieldValue($_params);
}
}
}
}
$attributes = array_merge(
[
'id' => $fieldId,
'name' => htmlspecialchars($itemName),
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
'data-formengine-input-name' => htmlspecialchars($itemName),
'rows' => (string)$rows,
'wrap' => (string)(($config['wrap'] ?? 'virtual') ?: 'virtual'),
],
$this->getOnFieldChangeAttrs('change', $parameterArray['fieldChangeFunc'] ?? [])
);
$classes = [
'form-control',
't3js-formengine-textarea',
'formengine-textarea',
];
if ($config['fixedFont'] ?? false) {
$classes[] = 'font-monospace';
}
if ($config['enableTabulator'] ?? false) {
$classes[] = 't3js-enable-tab';
}
$attributes['class'] = implode(' ', $classes);
if (isset($config['max']) && (int)$config['max'] > 0) {
$attributes['maxlength'] = (string)(int)$config['max'];
}
if (!empty($config['placeholder'])) {
$attributes['placeholder'] = htmlspecialchars(trim($config['placeholder']));
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$fieldWizardResult = $this->renderFieldWizard();
$fieldWizardHtml = $fieldWizardResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap" style="overflow: auto">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div hidden>';
$html[] = GeneralUtility::renderTextarea($itemValue, $attributes);
$html[] = '</div>';
$html[] = $this->getTableWizard($attributes['id']);
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = '<div class="btn-group">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
if (!empty($fieldWizardHtml)) {
$html[] = '<div class="form-wizards-item-bottom">';
$html[] = $fieldWizardHtml;
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
'@typo3/backend/form-engine/element/text-table-element.js'
)->instance($fieldId);
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/element/table-wizard-element.js');
$resultArray['additionalInlineLanguageLabelFiles'][] = 'EXT:core/Resources/Private/Language/locallang_wizards.xlf';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
/**
* Creates the HTML for the Table Wizard:
*
* @return string HTML for the table wizard
*/
protected function getTableWizard(string $dataId): string
{
$row = $this->data['databaseRow'];
$delimiter = (is_array($row['table_delimiter'] ?? '') && ($row['table_delimiter'][0] ?? false)) ? chr((int)$row['table_delimiter'][0]) : '|';
$enclosure = (is_array($row['table_enclosure'] ?? '') && ($row['table_enclosure'][0] ?? false)) ? chr((int)$row['table_enclosure'][0]) : '';
return sprintf(
'<typo3-formengine-table-wizard %s></typo3-formengine-table-wizard>',
GeneralUtility::implodeAttributes([
'type' => 'input',
'append-rows' => (string)$this->numNewRows,
'selector' => '#' . $dataId,
'delimiter' => $delimiter,
'enclosure' => $enclosure,
], true)
);
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
/**
* Generation of TCEform elements of where the type is unknown
*/
class UnknownElement extends AbstractFormElement
{
/**
* Handler for unknown types.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$resultArray = $this->initializeResultArray();
$type = $this->data['parameterArray']['fieldConf']['config']['type'];
$renderType = $this->data['renderType'];
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(
'<div class="alert alert-warning">Unknown type: <code>' . $type . '</code>' . ($renderType ? ', render type: <code>' . $renderType . '</code>' : '') . '</div>'
);
return $resultArray;
}
}
+49
View File
@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
/**
* Generation of elements of the type "user". This is a dummy implementation.
*
* type="user" elements should be combined with a custom renderType to create custom output.
* This implementation registered for type="user" kicks in if no renderType is given and is just
* a fallback implementation to hint developers that the TCA registration is incomplete.
*/
class UserElement extends AbstractFormElement
{
/**
* User defined field type
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
// Render some dummy output to explain this element should usually not be called at all.
$resultArray = $this->initializeResultArray();
$fieldName = $this->data['flexFormFieldName'] ?? $this->data['fieldName'];
$html = [];
$html[] = '<div class="alert alert-warning">';
$html[] = 'This is dummy output: Field <code>' . htmlspecialchars($fieldName) . '</code>';
$html[] = 'of table <code>' . htmlspecialchars($this->data['tableName']) . '</code>';
$html[] = ' is registered as type="user" element without a specific renderType.';
$html[] = ' Please look up details in TCA reference documentation for type="user".';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
}
@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
use TYPO3\CMS\Core\Resource\Exception\InvalidPathException;
use TYPO3\CMS\Core\Resource\StorageRepository;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Special type="user" element used in sys_file_storage is_public field
*
* @internal
*/
class UserSysFileStorageIsPublicElement extends AbstractFormElement
{
public function __construct(
private readonly FlashMessageService $flashMessageService,
private readonly StorageRepository $storageRepository,
) {}
/**
* There are some edge cases where "is_public" can never be marked as true in the BE,
* for instance, for storage located outside the document root or
* for storages driven by special driver such as Flickr, ...
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render(): array
{
$row = $this->data['databaseRow'];
$parameterArray = $this->data['parameterArray'];
$isPublic = (bool)$this->data['tcaSchemata']->get('sys_file_storage')->getField('is_public')->getDefaultValue();
if ($this->data['command'] === 'edit') {
// Make sure the storage object can be retrieved which is not the case when new storage.
$lang = $this->getLanguageService();
$defaultFlashMessageQueue = $this->flashMessageService->getMessageQueueByIdentifier();
try {
$storage = $this->storageRepository->findByUid((int)$row['uid']);
$storageRecord = $storage->getStorageRecord();
$isPublic = $storage->isPublic() && $storageRecord['is_public'];
// Display a warning to the BE User in case settings is not inline with storage capability.
if ($storageRecord['is_public'] && !$storage->isPublic()) {
$message = new FlashMessage(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.message.storage_is_no_public'),
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.header.storage_is_no_public'),
ContextualFeedbackSeverity::WARNING
);
$defaultFlashMessageQueue->enqueue($message);
}
} catch (InvalidPathException $e) {
$message = new FlashMessage(
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:filestorage.invalidpathexception.message'),
$lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:filestorage.invalidpathexception.title'),
ContextualFeedbackSeverity::ERROR
);
$defaultFlashMessageQueue->enqueue($message);
}
}
$isPublicAsString = $isPublic ? '1' : '0';
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->initializeResultArray();
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
$checkboxParameters = $this->checkBoxParams(
$parameterArray['itemFormElName'],
$isPublic ? 1 : 0,
0,
1,
$parameterArray['fieldChangeFunc'] ?? []
);
$checkboxId = htmlspecialchars(StringUtility::getUniqueId('formengine-fal-is-public-'));
$html = [];
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = '<div class="form-check form-switch">';
$html[] = '<input type="checkbox"';
$html[] = ' class="form-check-input"';
$html[] = ' value="1"';
$html[] = ' data-formengine-input-name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$html[] = ' id="' . $checkboxId . '"';
$html[] = $checkboxParameters;
$html[] = $isPublic ? ' checked="checked"' : '';
$html[] = '/>';
$html[] = '<label class="form-check-label" for="' . $checkboxId . '">';
$html[] = $this->appendValueToLabelInDebugMode('', $isPublicAsString);
$html[] = '</label>';
$html[] = '<input type="hidden"';
$html[] = ' name="' . htmlspecialchars($parameterArray['itemFormElName']) . '"';
$html[] = ' value="' . $isPublicAsString . '"';
$html[] = ' />';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = $this->wrapWithFieldsetAndLegend(implode(LF, $html));
return $resultArray;
}
}
+129
View File
@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/*
* This file is part of the TYPO3 CMS project.
*
* 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 TYPO3\CMS\Backend\Form\Element;
use Symfony\Component\Uid\Uuid;
use TYPO3\CMS\Core\Imaging\IconFactory;
use TYPO3\CMS\Core\Imaging\IconSize;
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
/**
* Render a readonly input field, which is filled with a UUID
*/
class UuidElement extends AbstractFormElement
{
public function __construct(
private readonly IconFactory $iconFactory,
) {}
public function render(): array
{
$resultArray = $this->initializeResultArray();
$parameterArray = $this->data['parameterArray'];
$itemValue = htmlspecialchars((string)$parameterArray['itemFormElValue'], ENT_QUOTES);
$config = $parameterArray['fieldConf']['config'];
$itemName = $parameterArray['itemFormElName'];
$fieldId = StringUtility::getUniqueId('formengine-uuid-');
if (!isset($config['required'])) {
$config['required'] = true;
}
if ($config['required'] && !Uuid::isValid($itemValue)) {
// Note: This can only happen in case the TcaUuid data provider is not executed or a custom
// data provider has changed the value afterwards. Since this can only happen in user code,
// we throw an exception to inform the administrator about this misconfiguration.
throw new \RuntimeException(
'Field "' . $this->data['fieldName'] . '" in table "' . $this->data['tableName'] . '" of type "uuid" defines the field to be required but does not contain a valid uuid. Make sure to properly generate a valid uuid value.',
1678895476
);
}
$width = $this->formMaxWidth(
MathUtility::forceIntegerInRange($config['size'] ?? $this->defaultInputWidth, $this->minimumInputWidth, $this->maxInputWidth)
);
$attributes = [
'id' => $fieldId,
'name' => $itemName,
'type' => 'text',
'readonly' => 'readonly',
'class' => 'form-control disabled',
'data-formengine-input-name' => $itemName,
'data-formengine-validation-rules' => $this->getValidationDataAsJsonString($config),
];
$uuidElement = '
<input value="' . $itemValue . '"
' . GeneralUtility::implodeAttributes($attributes, true) . '
/>';
$fieldInformationResult = $this->renderFieldInformation();
$fieldInformationHtml = $fieldInformationResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
if (($config['enableCopyToClipboard'] ?? true) !== false) {
$uuidElement = '
<div class="input-group">
' . $uuidElement . '
<typo3-copy-to-clipboard
class="btn btn-default"
title="' . htmlspecialchars(sprintf($this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_copytoclipboard.xlf:copyToClipboard.title'), 'UUID')) . '"
text="' . $itemValue . '"
>
' . $this->iconFactory->getIcon('actions-clipboard', IconSize::SMALL) . '
</typo3-copy-to-clipboard>
</div>';
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/copy-to-clipboard.js');
}
$fieldControlResult = $this->renderFieldControl();
$fieldControlHtml = $fieldControlResult['html'];
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldControlResult, false);
$html = [];
$html[] = $this->renderLabel($fieldId);
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
$html[] = $fieldInformationHtml;
$html[] = '<div class="form-control-wrap" style="max-width: ' . $width . 'px">';
$html[] = '<div class="form-wizards-wrap">';
$html[] = '<div class="form-wizards-item-element">';
$html[] = $uuidElement;
$html[] = '</div>';
if (!empty($fieldControlHtml)) {
$html[] = '<div class="form-wizards-item-aside form-wizards-item-aside--field-control">';
$html[] = '<div class="btn-group">';
$html[] = $fieldControlHtml;
$html[] = '</div>';
$html[] = '</div>';
}
$html[] = '</div>';
$html[] = '</div>';
$html[] = '</div>';
$resultArray['html'] = implode(LF, $html);
return $resultArray;
}
}