TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
<?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;
|
||||
|
||||
use TYPO3\CMS\Core\Domain\DateTimeFormat;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Base class for container and single elements - their abstracts extend from here.
|
||||
*/
|
||||
abstract class AbstractNode implements NodeInterface
|
||||
{
|
||||
/**
|
||||
* Main data array to work on, given from parent to child elements
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* A list of default field information added to the element / container.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldInformation = [];
|
||||
|
||||
/**
|
||||
* A list of default field controls added to the element / container.
|
||||
* This property is often reset by single elements.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldControl = [];
|
||||
|
||||
/**
|
||||
* A list of default field wizards added to the element / container.
|
||||
* This property is often reset by single elements.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldWizard = [];
|
||||
|
||||
/**
|
||||
* Retrieve the current data array from NodeFactory.
|
||||
*/
|
||||
public function setData(array $data): void
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for single nodes
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
abstract public function render(): array;
|
||||
|
||||
/**
|
||||
* Initialize the array that is returned to parent after calling. This structure
|
||||
* is identical for *all* nodes. Parent will merge the return of a child with its
|
||||
* own stuff and in itself return an array of the same structure.
|
||||
*
|
||||
* @return array{
|
||||
* additionalInlineLanguageLabelFiles: list<string>,
|
||||
* stylesheetFiles: list<string>,
|
||||
* javaScriptModules: list<JavaScriptModuleInstruction>,
|
||||
* inlineData: array<string, mixed>,
|
||||
* html: string,
|
||||
* }
|
||||
*/
|
||||
protected function initializeResultArray(): array
|
||||
{
|
||||
return [
|
||||
'additionalInlineLanguageLabelFiles' => [],
|
||||
'stylesheetFiles' => [],
|
||||
'javaScriptModules' => [],
|
||||
'inlineData' => [],
|
||||
'html' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge existing data with a child return array.
|
||||
* The incoming $childReturn array should be initialized
|
||||
* using initializeResultArray() beforehand.
|
||||
*
|
||||
* @param array $existing Currently merged array
|
||||
* @param array $childReturn Array returned by child
|
||||
* @param bool $mergeHtml If false, the ['html'] section of $childReturn will NOT be added to $existing
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function mergeChildReturnIntoExistingResult(array $existing, array $childReturn, bool $mergeHtml = true): array
|
||||
{
|
||||
if ($mergeHtml && !empty($childReturn['html'])) {
|
||||
$existing['html'] .= LF . $childReturn['html'];
|
||||
}
|
||||
foreach ($childReturn['stylesheetFiles'] ?? [] as $value) {
|
||||
$existing['stylesheetFiles'][] = $value;
|
||||
}
|
||||
foreach ($childReturn['javaScriptModules'] ?? [] as $module) {
|
||||
$existing['javaScriptModules'][] = $module;
|
||||
}
|
||||
foreach ($childReturn['additionalInlineLanguageLabelFiles'] ?? [] as $inlineLanguageLabelFile) {
|
||||
$existing['additionalInlineLanguageLabelFiles'][] = $inlineLanguageLabelFile;
|
||||
}
|
||||
if (!empty($childReturn['inlineData'])) {
|
||||
$existingInlineData = $existing['inlineData'];
|
||||
$childInlineData = $childReturn['inlineData'];
|
||||
ArrayUtility::mergeRecursiveWithOverrule($existingInlineData, $childInlineData);
|
||||
$existing['inlineData'] = $existingInlineData;
|
||||
}
|
||||
return $existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build JSON string for validations rules.
|
||||
*/
|
||||
protected function getValidationDataAsJsonString(array $config): string
|
||||
{
|
||||
$validationRules = [];
|
||||
if (!empty($config['eval'])) {
|
||||
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
|
||||
foreach ($evalList as $evalType) {
|
||||
$validationRules[] = [
|
||||
'type' => $evalType,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (!empty($config['range'])) {
|
||||
$newValidationRule = [
|
||||
'type' => 'range',
|
||||
];
|
||||
|
||||
$isDateTime = ($config['type'] ?? '') === 'datetime';
|
||||
if (!empty($config['range']['lower'])) {
|
||||
$lower = (int)$config['range']['lower'];
|
||||
if ($isDateTime) {
|
||||
$lower = date(DateTimeFormat::ISO8601_LOCALTIME, $lower);
|
||||
}
|
||||
$newValidationRule['lower'] = $lower;
|
||||
}
|
||||
if (!empty($config['range']['upper'])) {
|
||||
$upper = (int)$config['range']['upper'];
|
||||
if ($isDateTime) {
|
||||
$upper = date(DateTimeFormat::ISO8601_LOCALTIME, $upper);
|
||||
}
|
||||
$newValidationRule['upper'] = $upper;
|
||||
}
|
||||
$validationRules[] = $newValidationRule;
|
||||
}
|
||||
if (!empty($config['maxitems']) || !empty($config['minitems'])) {
|
||||
$minItems = isset($config['minitems']) ? (int)$config['minitems'] : 0;
|
||||
$maxItems = isset($config['maxitems']) ? (int)$config['maxitems'] : 99999;
|
||||
$type = $config['type'] ?: 'range';
|
||||
$validationRules[] = [
|
||||
'type' => $type,
|
||||
'minItems' => $minItems,
|
||||
'maxItems' => $maxItems,
|
||||
];
|
||||
}
|
||||
if (!empty($config['required'])) {
|
||||
$validationRules[] = ['type' => 'required'];
|
||||
}
|
||||
if (!empty($config['min'])) {
|
||||
$validationRules[] = ['type' => 'min'];
|
||||
}
|
||||
return json_encode($validationRules);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?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\Behavior;
|
||||
|
||||
interface OnFieldChangeInterface
|
||||
{
|
||||
/**
|
||||
* @return array{name: string, data: array<string, mixed>}
|
||||
*/
|
||||
public function toArray(): array;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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\Behavior;
|
||||
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
trait OnFieldChangeTrait
|
||||
{
|
||||
/**
|
||||
* @param OnFieldChangeInterface[] $items `fieldChangeFunc` items
|
||||
* @return array<int, array>
|
||||
*/
|
||||
protected function getOnFieldChangeItems(array $items): array
|
||||
{
|
||||
if ($items === []) {
|
||||
return [];
|
||||
}
|
||||
return array_map(
|
||||
static function (OnFieldChangeInterface $item): array {
|
||||
return $item->toArray();
|
||||
},
|
||||
array_values($items)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $event target client event, either `change` or `click`
|
||||
* @param list<OnFieldChangeInterface> $items `fieldChangeFunc` items
|
||||
* @return array<string, string> HTML attrs, not encoded - consumers MUST encode with `htmlspecialchars`
|
||||
*/
|
||||
protected function getOnFieldChangeAttrs(string $event, array $items): array
|
||||
{
|
||||
if ($items === []) {
|
||||
return [];
|
||||
}
|
||||
$onFieldChangeItems = $this->getOnFieldChangeItems($items);
|
||||
return [
|
||||
'data-formengine-field-change-event' => $event,
|
||||
'data-formengine-field-change-items' => GeneralUtility::jsonEncodeForHtmlAttribute($onFieldChangeItems, false),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards URL query params for `LinkBrowserController`
|
||||
* @param list<OnFieldChangeInterface> $items `fieldChangeFunc` items
|
||||
* @return array{fieldChangeFunc: array<int, array>, fieldChangeFuncHash: string} relevant URL query params for `LinkBrowserController`
|
||||
*/
|
||||
protected function forwardOnFieldChangeQueryParams(array $items): array
|
||||
{
|
||||
$func = $this->getOnFieldChangeItems($items);
|
||||
$hashService = GeneralUtility::makeInstance(HashService::class);
|
||||
return [
|
||||
'fieldChangeFunc' => $func,
|
||||
'fieldChangeFuncHash' => $hashService->hmac(serialize($func), 'backend-link-browser'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?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\Behavior;
|
||||
|
||||
/**
|
||||
* Provides reload behavior in form view,
|
||||
* in case a particular field has been changed.
|
||||
*/
|
||||
class ReloadOnFieldChange implements OnFieldChangeInterface
|
||||
{
|
||||
protected bool $confirmation;
|
||||
|
||||
public function __construct(bool $confirmation)
|
||||
{
|
||||
$this->confirmation = $confirmation;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'typo3-backend-form-reload',
|
||||
'data' => [
|
||||
'confirmation' => $this->confirmation,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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\Behavior;
|
||||
|
||||
/**
|
||||
* Updates bitmask values for multi-checkboxes.
|
||||
*/
|
||||
class UpdateBitmaskOnFieldChange implements OnFieldChangeInterface
|
||||
{
|
||||
protected int $position;
|
||||
protected int $total;
|
||||
protected bool $invert;
|
||||
protected string $elementName;
|
||||
|
||||
public function __construct(int $position, int $total, bool $invert, string $elementName)
|
||||
{
|
||||
$this->position = $position;
|
||||
$this->total = $total;
|
||||
$this->invert = $invert;
|
||||
$this->elementName = $elementName;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'typo3-backend-form-update-bitmask',
|
||||
'data' => [
|
||||
'position' => $this->position,
|
||||
'total' => $this->total,
|
||||
'invert' => $this->invert,
|
||||
'elementName' => $this->elementName,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?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\Behavior;
|
||||
|
||||
/**
|
||||
* Updates `TBE_EDITOR` value (the default action),
|
||||
* in case a particular field has been changed.
|
||||
*/
|
||||
class UpdateValueOnFieldChange implements OnFieldChangeInterface
|
||||
{
|
||||
protected string $tableName;
|
||||
protected string $identifier;
|
||||
protected string $fieldName;
|
||||
protected string $elementName;
|
||||
|
||||
public function __construct(string $tableName, string $identifier, string $fieldName, string $elementName)
|
||||
{
|
||||
$this->tableName = $tableName;
|
||||
$this->identifier = $identifier;
|
||||
$this->fieldName = $fieldName;
|
||||
$this->elementName = $elementName;
|
||||
}
|
||||
|
||||
public function withElementName(string $elementName): self
|
||||
{
|
||||
if ($this->elementName === $elementName) {
|
||||
return $this;
|
||||
}
|
||||
$target = clone $this;
|
||||
$target->elementName = $elementName;
|
||||
return $target;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'typo3-backend-form-update-value',
|
||||
'data' => [
|
||||
'tableName' => $this->tableName,
|
||||
'identifier' => $this->identifier,
|
||||
'fieldName' => $this->fieldName,
|
||||
'elementName' => $this->elementName,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\Form\NodeFactory;
|
||||
use TYPO3\CMS\Backend\View\BackendViewFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Abstract container has various methods used by the container classes
|
||||
*/
|
||||
abstract class AbstractContainer extends AbstractNode
|
||||
{
|
||||
protected NodeFactory $nodeFactory;
|
||||
protected BackendViewFactory $backendViewFactory;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
public function injectBackendViewFactory(BackendViewFactory $backendViewFactory)
|
||||
{
|
||||
$this->backendViewFactory = $backendViewFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge field information configuration with default and render them.
|
||||
*
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function renderFieldInformation(): array
|
||||
{
|
||||
$options = $this->data;
|
||||
$fieldInformation = $this->defaultFieldInformation;
|
||||
$currentRenderType = $this->data['renderType'];
|
||||
$fieldInformationFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['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;
|
||||
$currentRenderType = $this->data['renderType'];
|
||||
$fieldControlFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['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;
|
||||
$currentRenderType = $this->data['renderType'];
|
||||
$fieldWizardFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldWizard'] ?? [];
|
||||
ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca);
|
||||
$options['renderType'] = 'fieldWizard';
|
||||
$options['renderData']['fieldWizard'] = $fieldWizard;
|
||||
return $this->nodeFactory->create($options)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* A single field of TCA 'types' 'showitem' can have three semicolon separated configuration options:
|
||||
* fieldName: Name of the field to be found in TCA 'columns' section
|
||||
* fieldLabel: An alternative field label
|
||||
* paletteName: Name of a palette to be found in TCA 'palettes' section that is rendered after this field
|
||||
*
|
||||
* @param string $field Semicolon separated field configuration
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function explodeSingleFieldShowItemConfiguration(string $field): array
|
||||
{
|
||||
$fieldArray = GeneralUtility::trimExplode(';', $field);
|
||||
if (empty($fieldArray[0])) {
|
||||
throw new \RuntimeException('Field must not be empty', 1426448465);
|
||||
}
|
||||
return [
|
||||
'fieldName' => $fieldArray[0],
|
||||
'fieldLabel' => !empty($fieldArray[1]) ? $fieldArray[1] : null,
|
||||
'paletteName' => !empty($fieldArray[2]) ? $fieldArray[2] : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render tabs with label and content. Used by TabsContainer and FlexFormTabsContainer.
|
||||
* Re-uses the template Tabs.fluid.html which is also used by ModuleTemplate.php.
|
||||
*
|
||||
* @param array $menuItems Tab elements, each element is an array with "label" and "content"
|
||||
* @param string $domId DOM id attribute, will be appended with an iteration number per tab.
|
||||
*/
|
||||
protected function renderTabMenu(array $menuItems, string $domId): string
|
||||
{
|
||||
$view = $this->backendViewFactory->create($this->data['request']);
|
||||
$view->assignMultiple([
|
||||
'id' => $domId,
|
||||
'items' => $menuItems,
|
||||
'defaultTabIndex' => 1,
|
||||
'wrapContent' => false,
|
||||
'storeLastActiveTab' => true,
|
||||
]);
|
||||
return $view->render('Form/Tabs');
|
||||
}
|
||||
|
||||
protected function wrapWithFieldsetAndLegend(string $fieldContent): string
|
||||
{
|
||||
$legend = htmlspecialchars($this->data['parameterArray']['fieldConf']['label']);
|
||||
if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) {
|
||||
$fieldName = $this->data['flexFormContainerFieldName'] ?? $this->data['flexFormFieldName'] ?? $this->data['fieldName'];
|
||||
$legend .= ' <code>[' . htmlspecialchars($fieldName) . ']</code>';
|
||||
}
|
||||
$description = $this->renderDescription();
|
||||
return '<fieldset><legend class="form-label t3js-formengine-label">' . $legend . '</legend>' . $description . $fieldContent . '</fieldset>';
|
||||
}
|
||||
|
||||
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>';
|
||||
}
|
||||
|
||||
protected function getBackendUserAuthentication(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
<?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\Container;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceControlsEvent;
|
||||
use TYPO3\CMS\Backend\Form\Event\ModifyFileReferenceEnabledControlsEvent;
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Render a single file reference.
|
||||
*
|
||||
* This container is called by FilesControlContainer to render a single file (reference). The container is also
|
||||
* called by FormEngine for an incoming ajax request to expand an existing file (reference) or to create a new one.
|
||||
*
|
||||
* This container creates the outer HTML of single file (references) - e.g. drag and drop and delete buttons.
|
||||
* For rendering of the record itself processing is handed over to FullRecordContainer.
|
||||
*/
|
||||
class FileReferenceContainer extends AbstractContainer
|
||||
{
|
||||
private const string FILE_REFERENCE_TABLE = 'sys_file_reference';
|
||||
private const string FOREIGN_SELECTOR = 'uid_local';
|
||||
|
||||
/**
|
||||
* File reference data used for JSON output
|
||||
*/
|
||||
protected array $fileReferenceData = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly IconFactory $iconFactory,
|
||||
private readonly InlineStackProcessor $inlineStackProcessor,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly ResourceFactory $resourceFactory,
|
||||
private readonly ConnectionPool $connectionPool,
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
) {}
|
||||
|
||||
public function render(): array
|
||||
{
|
||||
// Send a mapping information to the browser via JSON:
|
||||
// e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
|
||||
$formPrefix = $this->inlineStackProcessor->getFormPrefixFromStructure($this->data['inlineStructure']);
|
||||
$domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid']);
|
||||
|
||||
$this->fileReferenceData = $this->data['inlineData'];
|
||||
$this->fileReferenceData['map'][$formPrefix] = $domObjectId;
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
$resultArray['inlineData'] = $this->fileReferenceData;
|
||||
|
||||
$html = '';
|
||||
$classes = [];
|
||||
$combinationHtml = '';
|
||||
$record = $this->data['databaseRow'];
|
||||
$uid = $record['uid'] ?? 0;
|
||||
$appendFormFieldNames = '[' . self::FILE_REFERENCE_TABLE . '][' . $uid . ']';
|
||||
$objectId = $domObjectId . '-' . self::FILE_REFERENCE_TABLE . '-' . $uid;
|
||||
$isNewRecord = $this->data['command'] === 'new';
|
||||
$hiddenFieldName = (string)($this->data['processedTca']['ctrl']['enablecolumns']['disabled'] ?? '');
|
||||
if (!$this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
if ($isNewRecord || $this->data['isInlineChildExpanded']) {
|
||||
$fileReferenceData = $this->renderFileReference($this->data);
|
||||
$html = $fileReferenceData['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fileReferenceData, false);
|
||||
} else {
|
||||
// This class is the marker for the JS-function to check if the full content has already been loaded
|
||||
$classes[] = 't3js-not-loaded';
|
||||
}
|
||||
if ($isNewRecord) {
|
||||
// Add pid of file reference as hidden field
|
||||
$html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[pid]" value="' . htmlspecialchars((string)$record['pid']) . '"/>';
|
||||
// Tell DataHandler this file reference is expanded
|
||||
$ucFieldName = 'uc[inlineView]'
|
||||
. '[' . $this->data['inlineTopMostParentTableName'] . ']'
|
||||
. '[' . $this->data['inlineTopMostParentUid'] . ']'
|
||||
. htmlspecialchars($appendFormFieldNames);
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
|
||||
. '" value="' . (int)$this->data['isInlineChildExpanded'] . '" />';
|
||||
} else {
|
||||
// Set additional field for processing for saving
|
||||
$html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[delete]" value="1" disabled="disabled" />';
|
||||
if ($hiddenFieldName !== ''
|
||||
&& (!$this->data['isInlineChildExpanded']
|
||||
|| !in_array($hiddenFieldName, $this->data['columnsToProcess'], true))
|
||||
) {
|
||||
$isHidden = (bool)($record[$hiddenFieldName] ?? false);
|
||||
$html .= '<input type="checkbox" class="d-none" data-formengine-input-name="data'
|
||||
. htmlspecialchars($appendFormFieldNames)
|
||||
. '[' . htmlspecialchars($hiddenFieldName) . ']" value="1"'
|
||||
. ($isHidden ? ' checked="checked"' : '') . ' />';
|
||||
$html .= '<input type="input" class="d-none" name="data' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[' . htmlspecialchars($hiddenFieldName) . ']" value="' . (int)$isHidden . '" />';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($this->data['inlineParentConfig']['renderFieldsOnly'] ?? false) {
|
||||
// Render "body" part only
|
||||
$resultArray['html'] = $html . $combinationHtml;
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
// Render header row and content (if expanded)
|
||||
if ($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$classes[] = 'panel-placeholder';
|
||||
}
|
||||
if ($record[$hiddenFieldName] ?? false) {
|
||||
$classes[] = 'panel-hidden';
|
||||
}
|
||||
if ($isNewRecord) {
|
||||
$classes[] = 'inlineIsNewRecord';
|
||||
}
|
||||
|
||||
// The hashed object id needs a non-numeric prefix, the value is used as ID selector in JavaScript
|
||||
$hashedObjectId = 'hash-' . md5($objectId);
|
||||
$containerAttributes = [
|
||||
'id' => $objectId . '_div',
|
||||
'class' => 'form-irre-object panel panel-default ' . trim(implode(' ', $classes)),
|
||||
'data-object-uid' => $record['uid'] ?? 0,
|
||||
'data-object-id' => $objectId,
|
||||
'data-object-id-hash' => $hashedObjectId,
|
||||
'data-object-parent-group' => $domObjectId . '-' . self::FILE_REFERENCE_TABLE,
|
||||
'data-field-name' => $appendFormFieldNames,
|
||||
'data-topmost-parent-table' => $this->data['inlineTopMostParentTableName'],
|
||||
'data-topmost-parent-uid' => $this->data['inlineTopMostParentUid'],
|
||||
'data-placeholder-record' => $this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ? '1' : '0',
|
||||
];
|
||||
|
||||
$isExpanded = $this->data['isInlineChildExpanded'] ?? false;
|
||||
$ariaControls = htmlspecialchars($objectId . '_fields', ENT_QUOTES | ENT_HTML5);
|
||||
$resultArray['html'] = '
|
||||
<div ' . GeneralUtility::implodeAttributes($containerAttributes, true) . '>
|
||||
<div class="panel-heading">
|
||||
<div class="panel-heading-row">
|
||||
' . $this->renderFileHeader($isExpanded, $ariaControls) . '
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-collapse collapse' . ($isExpanded ? ' show' : '') . '" id="' . $ariaControls . '">' . $html . $combinationHtml . '</div>
|
||||
</div>';
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function renderFileReference(array $data): array
|
||||
{
|
||||
$data['tabAndInlineStack'][] = [
|
||||
'inline',
|
||||
$this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $data['inlineFirstPid'])
|
||||
. '-'
|
||||
. $data['tableName']
|
||||
. '-'
|
||||
. $data['databaseRow']['uid'],
|
||||
];
|
||||
|
||||
return $this->nodeFactory->create(array_replace_recursive($data, [
|
||||
'inlineData' => $this->fileReferenceData,
|
||||
'renderType' => 'fullRecordContainer',
|
||||
]))->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML header for the file, such as the title, toggle-function, drag'n'drop, etc.
|
||||
* Later on the command-icons are inserted here, too.
|
||||
*/
|
||||
protected function renderFileHeader(bool $isExpanded, string $ariaControls): string
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$databaseRow = $this->data['databaseRow'];
|
||||
$recordTitle = $this->getRecordTitle();
|
||||
|
||||
if (empty($recordTitle)) {
|
||||
$recordTitle = '<em>[' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>';
|
||||
}
|
||||
|
||||
$objectId = htmlspecialchars($this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid'])
|
||||
. '-' . self::FILE_REFERENCE_TABLE
|
||||
. '-' . ($databaseRow['uid'] ?? 0));
|
||||
|
||||
$altText = BackendUtility::getRecordIconAltText($databaseRow, self::FILE_REFERENCE_TABLE, false);
|
||||
|
||||
// Renders the header image (thumbnail, icon, or missing file indicator)
|
||||
$headerImage = '';
|
||||
$headerBadge = '';
|
||||
$isMissing = false;
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] ?? false) {
|
||||
$fileUid = $databaseRow[self::FOREIGN_SELECTOR][0]['uid'] ?? null;
|
||||
if (!empty($fileUid)) {
|
||||
try {
|
||||
$fileObject = $this->resourceFactory->getFileObject($fileUid);
|
||||
if ($fileObject->isMissing()) {
|
||||
$isMissing = true;
|
||||
$recordTitle = htmlspecialchars($fileObject->getName());
|
||||
$headerImage = '
|
||||
<div class="panel-icon" id="' . $objectId . '_iconcontainer">
|
||||
' . $this->iconFactory->getIcon('default-not-found', IconSize::SMALL)->render() . '
|
||||
</div>';
|
||||
$headerBadge = '
|
||||
<div class="panel-badge">
|
||||
<span class="badge badge-danger">'
|
||||
. htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')) . '
|
||||
</span>
|
||||
</div>';
|
||||
} elseif ($fileObject->isImage() || $fileObject->isMediaFile()) {
|
||||
$imageSetup = $this->data['inlineParentConfig']['appearance']['headerThumbnail'] ?? [];
|
||||
$cropVariantCollection = CropVariantCollection::create($databaseRow['crop'] ?? '');
|
||||
if (!$cropVariantCollection->getCropArea()->isEmpty()) {
|
||||
$imageSetup['crop'] = $cropVariantCollection->getCropArea()->makeAbsoluteBasedOnFile($fileObject);
|
||||
}
|
||||
$processedImage = $fileObject->process(
|
||||
ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
|
||||
array_merge(['maxWidth' => 60, 'maxHeight' => 45], $imageSetup)
|
||||
);
|
||||
// Only use a thumbnail if the processing process was successful by checking if image width is set
|
||||
if ($processedImage->getProperty('width')) {
|
||||
$imageUrl = $processedImage->getPublicUrl() ?? '';
|
||||
$headerImage = '
|
||||
<div class="panel-thumbnail" id="' . $objectId . '_thumbnailcontainer">
|
||||
<img src="' . htmlspecialchars($imageUrl) . '" '
|
||||
. 'width="' . $processedImage->getProperty('width') . '" '
|
||||
. 'height="' . $processedImage->getProperty('height') . '" '
|
||||
. 'alt="" '
|
||||
. 'title="' . htmlspecialchars($altText) . '" '
|
||||
. 'loading="lazy">
|
||||
</div>';
|
||||
}
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$fileObject = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($headerImage === '' && !$isMissing) {
|
||||
$headerImage = '
|
||||
<div class="panel-icon" id="' . $objectId . '_iconcontainer">
|
||||
' . $this->iconFactory
|
||||
->getIconForRecord(self::FILE_REFERENCE_TABLE, $databaseRow, IconSize::SMALL)
|
||||
->setTitle($altText)
|
||||
->render() . '
|
||||
</div>';
|
||||
}
|
||||
|
||||
return '
|
||||
<button class="panel-button' . ($isExpanded ? '' : ' collapsed') . '" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#' . $ariaControls . '"
|
||||
aria-expanded="' . ($isExpanded ? 'true' : 'false') . '" aria-controls="' . $ariaControls . '">
|
||||
<span class="caret"></span>
|
||||
' . $headerImage . '
|
||||
<div class="panel-title"><span id="' . $objectId . '_label">' . $recordTitle . '</span></div>
|
||||
' . $headerBadge . '
|
||||
</button>
|
||||
<div class="panel-actions t3js-formengine-irre-control">
|
||||
' . $this->renderFileReferenceHeaderControl() . '
|
||||
</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the control-icons for a file reference (e.g. create new, sorting, delete, disable/enable).
|
||||
*/
|
||||
protected function renderFileReferenceHeaderControl(): string
|
||||
{
|
||||
$controls = [];
|
||||
$databaseRow = $this->data['databaseRow'];
|
||||
$databaseRow += [
|
||||
'uid' => 0,
|
||||
];
|
||||
$parentConfig = $this->data['inlineParentConfig'];
|
||||
$languageService = $this->getLanguageService();
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
$isNewItem = str_starts_with((string)$databaseRow['uid'], 'NEW');
|
||||
$fileReferenceTableTca = $this->data['tcaSchemata']->get(self::FILE_REFERENCE_TABLE);
|
||||
$calcPerms = new Permission(
|
||||
$backendUser->calcPerms(BackendUtility::readPageAccess(
|
||||
(int)($this->data['parentPageRow']['uid'] ?? 0),
|
||||
$backendUser->getPagePermsClause(Permission::PAGE_SHOW)
|
||||
))
|
||||
);
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new ModifyFileReferenceEnabledControlsEvent($this->data, $databaseRow)
|
||||
);
|
||||
if ($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$controls['localize'] = $this->iconFactory
|
||||
->getIcon('actions-edit-localize-status-low', IconSize::SMALL)
|
||||
->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize.isLocalizable'))
|
||||
->render();
|
||||
}
|
||||
if ($event->isControlEnabled('info')) {
|
||||
if ($isNewItem) {
|
||||
$controls['info'] = '
|
||||
<span class="btn btn-default disabled">
|
||||
' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . '
|
||||
</span>';
|
||||
} else {
|
||||
$controls['info'] = '
|
||||
<button type="button" class="btn btn-default" data-action="infowindow" data-info-table="' . htmlspecialchars('_FILE') . '" data-info-uid="' . (int)$databaseRow['uid_local'][0]['uid'] . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-document-info', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
}
|
||||
// If the table is NOT a read-only table, then show these links:
|
||||
if (!($parentConfig['readOnly'] ?? false)
|
||||
&& !($fileReferenceTableTca->getCapability(TcaSchemaCapability::AccessReadOnly)->getValue() ?? false)
|
||||
&& !($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)
|
||||
) {
|
||||
if ($event->isControlEnabled('sort')) {
|
||||
$icon = 'actions-move-up';
|
||||
$class = '';
|
||||
if ((int)$parentConfig['inline']['first'] === (int)$databaseRow['uid']) {
|
||||
$class = ' disabled';
|
||||
$icon = 'empty-empty';
|
||||
}
|
||||
$controls['sort.up'] = '
|
||||
<button type="button" class="btn btn-default' . $class . '" data-action="sort" data-direction="up" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveUp')) . '">
|
||||
' . $this->iconFactory->getIcon($icon, IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
|
||||
$icon = 'actions-move-down';
|
||||
$class = '';
|
||||
if ((int)$parentConfig['inline']['last'] === (int)$databaseRow['uid']) {
|
||||
$class = ' disabled';
|
||||
$icon = 'empty-empty';
|
||||
}
|
||||
$controls['sort.down'] = '
|
||||
<button type="button" class="btn btn-default' . $class . '" data-action="sort" data-direction="down" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveDown')) . '">
|
||||
' . $this->iconFactory->getIcon($icon, IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
$sysFileMetadataTableTca = $this->data['tcaSchemata']->has('sys_file_metadata') ? $this->data['tcaSchemata']->get('sys_file_metadata') : null;
|
||||
if (!$isNewItem
|
||||
&& ($languageField = ($sysFileMetadataTableTca?->getRawConfiguration()['languageField'] ?? false))
|
||||
&& $backendUser->check('tables_modify', 'sys_file_metadata')
|
||||
&& $event->isControlEnabled('edit')
|
||||
) {
|
||||
$languageId = (int)(is_array($databaseRow[$languageField] ?? null)
|
||||
? ($databaseRow[$languageField][0] ?? 0)
|
||||
: ($databaseRow[$languageField] ?? 0));
|
||||
$queryBuilder = $this->connectionPool->getQueryBuilderForTable('sys_file_metadata');
|
||||
$metadataRecord = $queryBuilder
|
||||
->select('uid')
|
||||
->from('sys_file_metadata')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'file',
|
||||
$queryBuilder->createNamedParameter((int)$databaseRow['uid_local'][0]['uid'], Connection::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
$languageField,
|
||||
$queryBuilder->createNamedParameter($languageId, Connection::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
if (!empty($metadataRecord)) {
|
||||
$url = (string)$this->uriBuilder->buildUriFromRoute('record_edit', [
|
||||
'edit[sys_file_metadata][' . (int)$metadataRecord['uid'] . ']' => 'edit',
|
||||
'module' => (string)($this->data['request']->getQueryParams()['module'] ?? ''),
|
||||
'returnUrl' => $this->data['returnUrl'],
|
||||
]);
|
||||
$controls['edit'] = '
|
||||
<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-open', IconSize::SMALL)->render() . '
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
if ($event->isControlEnabled('delete') && $calcPerms->editContentPermissionIsGranted()) {
|
||||
$recordInfo = $this->data['databaseRow']['uid_local'][0]['title'] ?? $this->data['recordTitle'] ?? '';
|
||||
if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) {
|
||||
$recordInfo .= ' [' . $this->data['tableName'] . ':' . $this->data['vanillaUid'] . ']';
|
||||
}
|
||||
$controls['delete'] = '
|
||||
<button type="button" class="btn btn-default t3js-editform-delete-inline-record" data-record-info="' . htmlspecialchars(trim($recordInfo)) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
if (($hiddenField = ($fileReferenceTableTca->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName())) !== ''
|
||||
&& ($fileReferenceTableTca->hasField($hiddenField))
|
||||
&& $event->isControlEnabled('hide')
|
||||
&& (
|
||||
!($fileReferenceTableTca->getField($hiddenField)->getConfiguration()['exclude'] ?? false)
|
||||
|| $backendUser->check('non_exclude_fields', self::FILE_REFERENCE_TABLE . ':' . $hiddenField)
|
||||
)
|
||||
) {
|
||||
if ($databaseRow[$hiddenField] ?? false) {
|
||||
$controls['hide'] = '
|
||||
<button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-edit-unhide', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
} else {
|
||||
$controls['hide'] = '
|
||||
<button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-edit-hide', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
}
|
||||
if (($parentConfig['appearance']['useSortable'] ?? false) && $event->isControlEnabled('dragdrop')) {
|
||||
$controls['dragdrop'] = '
|
||||
<span class="btn btn-default sortableHandle" data-id="' . (int)$databaseRow['uid'] . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-move-move', IconSize::SMALL)->render() . '
|
||||
</span>';
|
||||
}
|
||||
} elseif (($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)
|
||||
&& MathUtility::canBeInterpretedAsInteger($this->data['inlineParentUid'])
|
||||
&& $event->isControlEnabled('localize')
|
||||
) {
|
||||
$controls['localize'] = '
|
||||
<button type="button" class="btn btn-default t3js-synchronizelocalize-button" data-type="' . htmlspecialchars((string)$databaseRow['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-document-localize', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
if ($lockInfo = BackendUtility::isRecordLocked(self::FILE_REFERENCE_TABLE, $databaseRow['uid'])) {
|
||||
$controls['locked'] = '
|
||||
<button type="button" class="btn btn-default" title="' . htmlspecialchars($lockInfo['msg']) . '">
|
||||
' . $this->iconFactory->getIcon('status-user-backend', IconSize::SMALL, 'overlay-edit')->render() . '
|
||||
</button>';
|
||||
}
|
||||
|
||||
// Get modified controls. This means their markup was modified, new controls were added or controls got removed.
|
||||
$controls = $this->eventDispatcher->dispatch(
|
||||
new ModifyFileReferenceControlsEvent($controls, $this->data, $databaseRow)
|
||||
)->getControls();
|
||||
|
||||
$out = '';
|
||||
if (($controls['edit'] ?? false) || ($controls['hide'] ?? false) || ($controls['delete'] ?? false)) {
|
||||
$out .= '
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
' . ($controls['edit'] ?? '') . ($controls['hide'] ?? '') . ($controls['delete'] ?? '') . '
|
||||
</div>';
|
||||
unset($controls['edit'], $controls['hide'], $controls['delete']);
|
||||
}
|
||||
if (($controls['info'] ?? false) || ($controls['new'] ?? false) || ($controls['sort.up'] ?? false) || ($controls['sort.down'] ?? false) || ($controls['dragdrop'] ?? false)) {
|
||||
$out .= '
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
' . ($controls['info'] ?? '') . ($controls['new'] ?? '') . ($controls['sort.up'] ?? '') . ($controls['sort.down'] ?? '') . ($controls['dragdrop'] ?? '') . '
|
||||
</div>';
|
||||
unset($controls['info'], $controls['new'], $controls['sort.up'], $controls['sort.down'], $controls['dragdrop']);
|
||||
}
|
||||
if ($controls['localize'] ?? false) {
|
||||
$out .= '<div class="btn-group btn-group-sm" role="group">' . $controls['localize'] . '</div>';
|
||||
unset($controls['localize']);
|
||||
}
|
||||
if ($controls !== [] && ($remainingControls = trim(implode('', $controls))) !== '') {
|
||||
$out .= '<div class="btn-group btn-group-sm" role="group">' . $remainingControls . '</div>';
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
protected function getRecordTitle(): string
|
||||
{
|
||||
$databaseRow = $this->data['databaseRow'];
|
||||
$fileRecord = $databaseRow['uid_local'][0]['row'] ?? null;
|
||||
|
||||
if ($fileRecord === null) {
|
||||
return $this->data['recordTitle'] ?: (string)$databaseRow['uid'];
|
||||
}
|
||||
|
||||
$title = '<span>' . $this->getLabelFieldForRecord($databaseRow, $fileRecord, 'name') . '</span>';
|
||||
|
||||
// In debug mode, add the table name to the record title
|
||||
if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) {
|
||||
$title .= ' <span class="panel-meta"><code>[' . self::FILE_REFERENCE_TABLE . ']</code></span>';
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
protected function getLabelFieldForRecord(array $databaseRow, array $fileRecord, string $field): string
|
||||
{
|
||||
$value = '';
|
||||
|
||||
if (isset($databaseRow[$field])) {
|
||||
$value = htmlspecialchars((string)$databaseRow[$field]);
|
||||
} elseif (isset($fileRecord[$field])) {
|
||||
$value = htmlspecialchars((string)$fileRecord[$field]);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
<?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\Container;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Backend\Form\Event\CustomFileControlsEvent;
|
||||
use TYPO3\CMS\Backend\Form\Event\CustomFileSelectorsEvent;
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Resource\DefaultUploadFolderResolver;
|
||||
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Files entry container.
|
||||
*
|
||||
* This container is the entry step to rendering a file reference. It is created by SingleFieldContainer.
|
||||
*
|
||||
* The code creates the main structure for the single file reference, initializes the inlineData array,
|
||||
* that is manipulated and also returned in its manipulated state. The "control" stuff of file
|
||||
* references is rendered here, for example the "create new" button.
|
||||
*
|
||||
* For each existing file reference, a FileReferenceContainer is called for further processing.
|
||||
*/
|
||||
class FilesControlContainer extends AbstractContainer
|
||||
{
|
||||
private const string FILE_REFERENCE_TABLE = 'sys_file_reference';
|
||||
|
||||
/**
|
||||
* Inline data array used in JS, returned as JSON object to frontend
|
||||
*/
|
||||
protected array $fileReferenceData = [];
|
||||
|
||||
/**
|
||||
* @var array<int,JavaScriptModuleInstruction|string|array<string,string>>
|
||||
*/
|
||||
protected array $javaScriptModules = [];
|
||||
|
||||
protected $defaultFieldWizard = [
|
||||
'localizationStateSelector' => [
|
||||
'renderType' => 'localizationStateSelector',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly IconFactory $iconFactory,
|
||||
private readonly InlineStackProcessor $inlineStackProcessor,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly OnlineMediaHelperRegistry $onlineMediaHelperRegistry,
|
||||
private readonly DefaultUploadFolderResolver $defaultUploadFolderResolver,
|
||||
private readonly HashService $hashService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$this->fileReferenceData = $this->data['inlineData'];
|
||||
|
||||
$inlineStructure = $this->data['inlineStructure'];
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$field = $this->data['fieldName'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$config = $parameterArray['fieldConf']['config'];
|
||||
$isReadOnly = (bool)($config['readOnly'] ?? false);
|
||||
$language = 0;
|
||||
if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) {
|
||||
$languageFieldName = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
|
||||
$language = isset($row[$languageFieldName][0]) ? (int)$row[$languageFieldName][0] : (int)$row[$languageFieldName];
|
||||
}
|
||||
|
||||
// Add the current inline job to the structure stack
|
||||
$newStructureItem = [
|
||||
'table' => $table,
|
||||
'uid' => $row['uid'],
|
||||
'field' => $field,
|
||||
'config' => $config,
|
||||
];
|
||||
|
||||
// Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
|
||||
$itemName = (string)$parameterArray['itemFormElName'];
|
||||
if ($itemName !== '') {
|
||||
$flexFormParts = $this->extractFlexFormParts($itemName);
|
||||
if ($flexFormParts !== null) {
|
||||
$newStructureItem['flexform'] = $flexFormParts;
|
||||
if ($flexFormParts !== []
|
||||
&& isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'])
|
||||
) {
|
||||
// Transport the flexform DS identifier fields to the FormFilesAjaxController
|
||||
$config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$inlineStructure['stable'][] = $newStructureItem;
|
||||
|
||||
// Hand over original returnUrl to FormFilesAjaxController. Needed if opening for instance a
|
||||
// nested element in a new view to then go back to the original returnUrl and not the url of
|
||||
// the inline ajax controller
|
||||
$config['originalReturnUrl'] = $this->data['returnUrl'];
|
||||
|
||||
// e.g. data[<table>][<uid>][<field>]
|
||||
$formFieldName = $this->inlineStackProcessor->getFormPrefixFromStructure($inlineStructure);
|
||||
// e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
|
||||
$formFieldIdentifier = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']);
|
||||
|
||||
$inlineChildren = $parameterArray['fieldConf']['children'] ?? [];
|
||||
|
||||
$config['inline']['first'] = $config['inline']['last'] = false;
|
||||
if (is_array($inlineChildren) && $inlineChildren !== []) {
|
||||
$firstChild = array_first($inlineChildren);
|
||||
if (isset($firstChild['databaseRow']['uid'])) {
|
||||
$config['inline']['first'] = $firstChild['databaseRow']['uid'];
|
||||
}
|
||||
$lastChild = array_last($inlineChildren);
|
||||
if (isset($lastChild['databaseRow']['uid'])) {
|
||||
$config['inline']['last'] = $lastChild['databaseRow']['uid'];
|
||||
}
|
||||
}
|
||||
|
||||
$top = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0);
|
||||
|
||||
$this->fileReferenceData['config'][$formFieldIdentifier] = [
|
||||
'table' => self::FILE_REFERENCE_TABLE,
|
||||
];
|
||||
$configJson = (string)json_encode($config);
|
||||
$this->fileReferenceData['config'][$formFieldIdentifier . '-' . self::FILE_REFERENCE_TABLE] = [
|
||||
'min' => $config['minitems'] ?? null,
|
||||
'max' => $config['maxitems'] ?? null,
|
||||
'sortable' => $config['appearance']['useSortable'] ?? false,
|
||||
'top' => [
|
||||
'table' => $top['table'],
|
||||
'uid' => $top['uid'],
|
||||
],
|
||||
'context' => [
|
||||
'config' => $configJson,
|
||||
'hmac' => $this->hashService->hmac($configJson, 'FilesContext'),
|
||||
],
|
||||
];
|
||||
$this->fileReferenceData['nested'][$formFieldIdentifier] = $this->data['tabAndInlineStack'];
|
||||
|
||||
$resultArray['inlineData'] = $this->fileReferenceData;
|
||||
|
||||
// @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider
|
||||
$uidOfDefaultRecord = 0;
|
||||
if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) {
|
||||
$originPointerField = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
|
||||
$uidOfDefaultRecord = $row[$originPointerField] ?? 0;
|
||||
}
|
||||
$isLocalizedParent = $language > 0
|
||||
&& ($uidOfDefaultRecord[0] ?? $uidOfDefaultRecord) > 0
|
||||
&& MathUtility::canBeInterpretedAsInteger($row['uid']);
|
||||
$numberOfFullLocalizedChildren = 0;
|
||||
$numberOfNotYetLocalizedChildren = 0;
|
||||
foreach ($inlineChildren as $child) {
|
||||
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$numberOfFullLocalizedChildren++;
|
||||
}
|
||||
if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$numberOfNotYetLocalizedChildren++;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isReadOnly || $numberOfFullLocalizedChildren >= ($config['maxitems'] ?? 0)) {
|
||||
$config['inline']['showNewFileReferenceButton'] = false;
|
||||
$config['inline']['showCreateNewRelationButton'] = false;
|
||||
$config['inline']['showOnlineMediaAddButtonStyle'] = false;
|
||||
}
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
$fieldWizardResult = $this->renderFieldWizard();
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
|
||||
|
||||
$sortableRecordUids = $fileReferencesHtml = [];
|
||||
foreach ($inlineChildren as $options) {
|
||||
$options['inlineParentUid'] = $row['uid'];
|
||||
$options['inlineFirstPid'] = $this->data['inlineFirstPid'];
|
||||
$options['inlineParentConfig'] = $config;
|
||||
$options['inlineData'] = $this->fileReferenceData;
|
||||
$options['inlineStructure'] = $inlineStructure;
|
||||
$options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
|
||||
$options['renderType'] = 'fileReferenceContainer';
|
||||
$fileReference = $this->nodeFactory->create($options)->render();
|
||||
$fileReferencesHtml[] = $fileReference['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fileReference, false);
|
||||
if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext'] && isset($options['databaseRow']['uid'])) {
|
||||
// Don't add record to list of "valid" uids if it is only the default
|
||||
// language record of a not yet localized child
|
||||
$sortableRecordUids[] = $options['databaseRow']['uid'];
|
||||
}
|
||||
}
|
||||
|
||||
$view = $this->backendViewFactory->create($this->data['request']);
|
||||
$view->assignMultiple([
|
||||
'formFieldIdentifier' => $formFieldIdentifier,
|
||||
'formFieldName' => $formFieldName,
|
||||
'webComponentAttributes' => GeneralUtility::implodeAttributes([
|
||||
'id' => $formFieldIdentifier,
|
||||
'data-type' => 'file',
|
||||
'data-object-group' => $formFieldIdentifier . '-' . self::FILE_REFERENCE_TABLE,
|
||||
'data-form-field' => $formFieldName,
|
||||
'data-expand-single' => (bool)($config['appearance']['expandSingle'] ?? false) ? 'true' : 'false',
|
||||
'data-sortable' => (bool)($config['appearance']['useSortable'] ?? false) ? 'true' : 'false',
|
||||
'data-min' => (int)($config['minitems'] ?? 0),
|
||||
'data-max' => (int)($config['maxitems'] ?? 0),
|
||||
], true),
|
||||
'fieldInformation' => $fieldInformationResult['html'],
|
||||
'fieldWizard' => $fieldWizardResult['html'],
|
||||
'fileReferences' => [
|
||||
'id' => $formFieldIdentifier . '_records',
|
||||
'title' => $languageService->sL(trim($parameterArray['fieldConf']['label'] ?? '')),
|
||||
'records' => implode(LF, $fileReferencesHtml),
|
||||
],
|
||||
'sortableRecordUids' => implode(',', $sortableRecordUids),
|
||||
'validationRules' => $this->getValidationDataAsJsonString([
|
||||
'type' => 'inline',
|
||||
'minitems' => $config['minitems'] ?? null,
|
||||
'maxitems' => $config['maxitems'] ?? null,
|
||||
]),
|
||||
]);
|
||||
|
||||
if (!$isReadOnly && ($config['appearance']['showFileSelectors'] ?? true) !== false) {
|
||||
/** @var FileExtensionFilter $fileExtensionFilter */
|
||||
$fileExtensionFilter = GeneralUtility::makeInstance(FileExtensionFilter::class);
|
||||
$fileExtensionFilter->setAllowedFileExtensions($config['allowed'] ?? null);
|
||||
$fileExtensionFilter->setDisallowedFileExtensions($config['disallowed'] ?? null);
|
||||
$view->assign('fileSelectors', $this->getFileSelectors($inlineStructure, $config, $fileExtensionFilter));
|
||||
$filteredFileExtensions = $fileExtensionFilter->getFilteredFileExtensions();
|
||||
// Do not display "allowed file extensions" if all extensions are allowed (indicated by ['*'])
|
||||
if (($filteredFileExtensions['allowedFileExtensions'] ?? null) === ['*']) {
|
||||
$filteredFileExtensions = [];
|
||||
}
|
||||
$view->assignMultiple($filteredFileExtensions);
|
||||
// Render the localization buttons if needed
|
||||
if ($numberOfNotYetLocalizedChildren) {
|
||||
$view->assignMultiple([
|
||||
'showAllLocalizationLink' => !empty($config['appearance']['showAllLocalizationLink']),
|
||||
'showSynchronizationLink' => !empty($config['appearance']['showSynchronizationLink']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new CustomFileControlsEvent($resultArray, $table, $field, $row, $config, $formFieldIdentifier, $formFieldName)
|
||||
);
|
||||
$resultArray = $event->getResultArray();
|
||||
$controls = $event->getControls();
|
||||
|
||||
if ($controls !== []) {
|
||||
$view->assign('customControls', [
|
||||
'id' => $formFieldIdentifier . '_customControls',
|
||||
'controls' => implode("\n", $controls),
|
||||
]);
|
||||
}
|
||||
|
||||
$resultArray['javaScriptModules'] = array_merge(
|
||||
$resultArray['javaScriptModules'],
|
||||
$this->javaScriptModules,
|
||||
[JavaScriptModuleInstruction::create('@typo3/backend/form-engine/container/inline-control-container.js')]
|
||||
);
|
||||
|
||||
$resultArray['html'] = $this->wrapWithFieldsetAndLegend($view->render('Form/FilesControlContainer'));
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate buttons to select, reference and upload files.
|
||||
*/
|
||||
protected function getFileSelectors(array $inlineStructure, array $inlineConfiguration, FileExtensionFilter $fileExtensionFilter): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
|
||||
$currentStructureDomObjectIdPrefix = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']);
|
||||
$objectPrefix = $currentStructureDomObjectIdPrefix . '-' . self::FILE_REFERENCE_TABLE;
|
||||
|
||||
$controls = [];
|
||||
if ($inlineConfiguration['appearance']['elementBrowserEnabled'] ?? true) {
|
||||
if ($inlineConfiguration['appearance']['createNewRelationLinkTitle'] ?? false) {
|
||||
$buttonText = $inlineConfiguration['appearance']['createNewRelationLinkTitle'];
|
||||
} else {
|
||||
$buttonText = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation';
|
||||
}
|
||||
$buttonText = $languageService->sL($buttonText);
|
||||
$attributes = [
|
||||
'type' => 'button',
|
||||
'class' => 'btn btn-default t3js-element-browser',
|
||||
'hidden' => !($inlineConfiguration['inline']['showCreateNewRelationButton'] ?? true) ? 'hidden' : null,
|
||||
'title' => $buttonText,
|
||||
'data-mode' => 'file',
|
||||
'data-allowed-types' => implode(',', $fileExtensionFilter->getAllowedFileExtensions() ?? []),
|
||||
'data-disallowed-types' => implode(',', $fileExtensionFilter->getDisallowedFileExtensions() ?? []),
|
||||
'data-irre-object-id' => $objectPrefix,
|
||||
];
|
||||
$controls[] = '
|
||||
<button ' . GeneralUtility::implodeAttributes($attributes, true) . '>
|
||||
' . $this->iconFactory->getIcon('actions-insert-record', IconSize::SMALL)->render() . '
|
||||
' . htmlspecialchars($buttonText) . '
|
||||
</button>';
|
||||
}
|
||||
|
||||
$onlineMediaAllowed = [];
|
||||
foreach ($this->onlineMediaHelperRegistry->getSupportedFileExtensions() as $supportedFileExtension) {
|
||||
if ($fileExtensionFilter->isAllowed($supportedFileExtension)) {
|
||||
$onlineMediaAllowed[] = $supportedFileExtension;
|
||||
}
|
||||
}
|
||||
|
||||
$showUpload = (bool)($inlineConfiguration['appearance']['fileUploadAllowed'] ?? true);
|
||||
$showByUrl = ($inlineConfiguration['appearance']['fileByUrlAllowed'] ?? true) && $onlineMediaAllowed !== [];
|
||||
|
||||
if (($showUpload || $showByUrl) && $backendUser->getUserSettings()->isUploadFieldsInTopOfEBEnabled()) {
|
||||
$folder = $this->defaultUploadFolderResolver->resolve(
|
||||
$backendUser,
|
||||
$this->data['tableName'] === 'pages' ? $this->data['vanillaUid'] : ($this->data['parentPageRow']['uid'] ?? 0),
|
||||
$this->data['tableName'],
|
||||
$this->data['fieldName']
|
||||
);
|
||||
if (
|
||||
$folder instanceof Folder
|
||||
&& $folder->getStorage()->checkUserActionPermission('add', 'File')
|
||||
) {
|
||||
if ($showUpload) {
|
||||
if ($inlineConfiguration['appearance']['uploadFilesLinkTitle'] ?? false) {
|
||||
$buttonText = $inlineConfiguration['appearance']['uploadFilesLinkTitle'];
|
||||
} else {
|
||||
$buttonText = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.select-and-submit';
|
||||
}
|
||||
$buttonText = $languageService->sL($buttonText);
|
||||
|
||||
$attributes = [
|
||||
'type' => 'button',
|
||||
'class' => 'btn btn-default t3js-drag-uploader',
|
||||
'title' => $buttonText,
|
||||
'hidden' => !($inlineConfiguration['inline']['showCreateNewRelationButton'] ?? true) ? 'hidden' : null,
|
||||
'data-dropzone-target' => '#' . StringUtility::escapeCssSelector($currentStructureDomObjectIdPrefix),
|
||||
'data-insert-dropzone-before' => '1',
|
||||
'data-file-irre-object' => $objectPrefix,
|
||||
'data-file-allowed' => implode(',', $fileExtensionFilter->getAllowedFileExtensions() ?? []),
|
||||
'data-file-disallowed' => implode(',', $fileExtensionFilter->getDisallowedFileExtensions() ?? []),
|
||||
'data-target-folder' => $folder->getCombinedIdentifier(),
|
||||
'data-max-file-size' => (string)(GeneralUtility::getMaxUploadFileSize() * 1024),
|
||||
];
|
||||
$controls[] = '
|
||||
<button ' . GeneralUtility::implodeAttributes($attributes, true) . '>
|
||||
' . $this->iconFactory->getIcon('actions-upload', IconSize::SMALL)->render() . '
|
||||
' . htmlspecialchars($buttonText) . '
|
||||
</button>';
|
||||
|
||||
$this->javaScriptModules[] = JavaScriptModuleInstruction::create('@typo3/backend/drag-uploader.js');
|
||||
}
|
||||
if ($showByUrl) {
|
||||
if ($inlineConfiguration['appearance']['addMediaLinkTitle'] ?? false) {
|
||||
$buttonText = $inlineConfiguration['appearance']['addMediaLinkTitle'];
|
||||
} else {
|
||||
$buttonText = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.button';
|
||||
}
|
||||
$buttonText = $languageService->sL($buttonText);
|
||||
$attributes = [
|
||||
'type' => 'button',
|
||||
'class' => 'btn btn-default t3js-online-media-add-btn',
|
||||
'title' => $buttonText,
|
||||
'hidden' => !($inlineConfiguration['inline']['showOnlineMediaAddButtonStyle'] ?? true) ? 'hidden' : null,
|
||||
'data-target-folder' => $folder->getCombinedIdentifier(),
|
||||
'data-file-irre-object' => $objectPrefix,
|
||||
'data-online-media-allowed' => implode(',', $onlineMediaAllowed),
|
||||
'data-btn-submit' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder'),
|
||||
'data-placeholder' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder'),
|
||||
'data-online-media-allowed-help-text' => $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.allowEmbedSources'),
|
||||
];
|
||||
|
||||
// @todo Should be implemented as web component
|
||||
$controls[] = '
|
||||
<button ' . GeneralUtility::implodeAttributes($attributes, true) . '>
|
||||
' . $this->iconFactory->getIcon('actions-online-media-add', IconSize::SMALL)->render() . '
|
||||
' . htmlspecialchars($buttonText) . '
|
||||
</button>';
|
||||
|
||||
$this->javaScriptModules[] = JavaScriptModuleInstruction::create('@typo3/backend/online-media.js');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$event = $this->eventDispatcher->dispatch(
|
||||
new CustomFileSelectorsEvent($controls, $this->javaScriptModules, $this->data['tableName'], $this->data['fieldName'], $this->data['databaseRow'], $inlineConfiguration, $fileExtensionFilter, $objectPrefix)
|
||||
);
|
||||
$this->javaScriptModules = $event->getJavaScriptModules();
|
||||
return $event->getSelectors();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts FlexForm parts of a form element name like
|
||||
* data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
|
||||
*/
|
||||
protected function extractFlexFormParts(string $formElementName): ?array
|
||||
{
|
||||
$flexFormParts = null;
|
||||
$matches = [];
|
||||
if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
|
||||
$flexFormParts = GeneralUtility::trimExplode(
|
||||
'][',
|
||||
trim($matches[1], '[]')
|
||||
);
|
||||
}
|
||||
return $flexFormParts;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Flex form container implementation
|
||||
* This one is called by FlexFormSectionContainer and renders HTML for a single container.
|
||||
* For processing of single elements FlexFormElementContainer is called
|
||||
*/
|
||||
class FlexFormContainerContainer extends AbstractContainer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly IconFactory $iconFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$fieldName = $this->data['fieldName'];
|
||||
$flexFormFormPrefix = $this->data['flexFormFormPrefix'];
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
|
||||
$flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier'];
|
||||
$actionFieldName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'
|
||||
. $flexFormFormPrefix
|
||||
. '[' . $flexFormContainerIdentifier . ']'
|
||||
. '[_ACTION]';
|
||||
|
||||
$moveAndDeleteContent = [];
|
||||
$userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
|
||||
if ($userHasAccessToDefaultLanguage) {
|
||||
$moveAndDeleteContent[] = ''
|
||||
. '<button type="button" class="btn btn-default t3js-delete">'
|
||||
. $this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL)->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete'))->render()
|
||||
. '</button>';
|
||||
$moveAndDeleteContent[] = ''
|
||||
. '<button type="button" class="btn btn-default t3js-sortable-handle sortableHandle">'
|
||||
. $this->iconFactory->getIcon('actions-move-move', IconSize::SMALL)->setTitle($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:sortable.dragmove'))->render()
|
||||
. '</button>';
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
// Append container specific stuff to field prefix
|
||||
$options['flexFormFormPrefix'] = $flexFormFormPrefix . '[' . $flexFormContainerIdentifier . '][' . $this->data['flexFormContainerName'] . '][el]';
|
||||
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray['el'];
|
||||
$options['renderType'] = 'flexFormElementContainer';
|
||||
$containerContentResult = $this->nodeFactory->create($options)->render();
|
||||
|
||||
$containerTitle = '';
|
||||
if (!empty(trim($flexFormDataStructureArray['title']))) {
|
||||
$containerTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
|
||||
}
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$parentSectionContainer = sprintf('flexform-section-container-%s-%s-%s-%s', $this->data['flexFormSheetName'], $this->data['fieldName'], md5($this->data['flexFormFieldName']), md5($this->data['elementBaseName']));
|
||||
$flexFormDomContainerId = sprintf('%s-%s', $parentSectionContainer, $flexFormContainerIdentifier);
|
||||
$containerAttributes = [
|
||||
'class' => 'panel panel-default t3js-flex-section',
|
||||
'data-parent' => $parentSectionContainer,
|
||||
'data-flexform-container-id' => $flexFormContainerIdentifier,
|
||||
];
|
||||
|
||||
$panelHeaderAttributes = [
|
||||
'class' => 'panel-heading',
|
||||
];
|
||||
|
||||
$toggleAttributes = [
|
||||
'class' => 'panel-button collapsed',
|
||||
'type' => 'button',
|
||||
'data-bs-toggle' => 'collapse',
|
||||
'data-bs-target' => '#' . $flexFormDomContainerId,
|
||||
'aria-controls' => $flexFormDomContainerId,
|
||||
'aria-expanded' => 'false',
|
||||
];
|
||||
|
||||
$html = [];
|
||||
$html[] = '<div ' . GeneralUtility::implodeAttributes($containerAttributes, true) . '>';
|
||||
$html[] = '<div ' . GeneralUtility::implodeAttributes($panelHeaderAttributes, true) . '>';
|
||||
$html[] = '<div class="panel-heading-row">';
|
||||
$html[] = '<button ' . GeneralUtility::implodeAttributes($toggleAttributes, true) . '>';
|
||||
$html[] = '<span class="caret"></span>';
|
||||
$html[] = '<div class="panel-title">';
|
||||
$html[] = '<output class="content-preview"></output>';
|
||||
$html[] = '<span class="panel-meta">' . htmlspecialchars($containerTitle) . '</span>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</button>';
|
||||
$html[] = '<div class="panel-actions t3js-formengine-irre-control">';
|
||||
$html[] = '<div class="btn-group btn-group-sm">';
|
||||
$html[] = implode(LF, $moveAndDeleteContent);
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div id="' . htmlspecialchars($flexFormDomContainerId) . '" class="panel-collapse collapse t3js-flex-section-content">';
|
||||
$html[] = $containerContentResult['html'];
|
||||
$html[] = '</div>';
|
||||
$html[] = '<input class="t3js-flex-control-action" type="hidden" name="' . htmlspecialchars($actionFieldName) . '" value="" />';
|
||||
$html[] = '</div>';
|
||||
|
||||
$resultArray['html'] = implode(LF, $html);
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $containerContentResult, false);
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Behavior\ReloadOnFieldChange;
|
||||
use TYPO3\CMS\Backend\Form\Behavior\UpdateValueOnFieldChange;
|
||||
use TYPO3\CMS\Core\Authentication\JsConfirmation;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
|
||||
/**
|
||||
* The container handles single elements.
|
||||
*
|
||||
* This one is called by FlexFormTabsContainer, FlexFormNoTabsContainer or FlexFormContainerContainer.
|
||||
* For single fields, the code is similar to SingleFieldContainer, processing will end up in single
|
||||
* element classes depending on specific renderType of an element. Additionally, it determines if a
|
||||
* section is handled and hands over to FlexFormSectionContainer in this case.
|
||||
*/
|
||||
class FlexFormElementContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$flexFormRowData = $this->data['flexFormRowData'];
|
||||
$flexFormFormPrefix = $this->data['flexFormFormPrefix'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
|
||||
$languageService = $this->getLanguageService();
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
foreach ($flexFormDataStructureArray as $flexFormFieldName => $flexFormFieldArray) {
|
||||
if (
|
||||
// No item array found at all
|
||||
!is_array($flexFormFieldArray)
|
||||
// Not a section or container and not a list of single items
|
||||
|| (!isset($flexFormFieldArray['type']) && !is_array($flexFormFieldArray['config']))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (($flexFormFieldArray['type'] ?? null) === 'array') {
|
||||
// Section
|
||||
if (empty($flexFormFieldArray['section'])) {
|
||||
$resultArray['html'] = LF . 'Section expected at ' . $flexFormFieldName . ' but not found';
|
||||
continue;
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
$options['flexFormDataStructureArray'] = $flexFormFieldArray;
|
||||
$options['flexFormRowData'] = $flexFormRowData[$flexFormFieldName]['el'] ?? [];
|
||||
$options['flexFormFieldName'] = $flexFormFieldName;
|
||||
$options['renderType'] = 'flexFormSectionContainer';
|
||||
$sectionContainerResult = $this->nodeFactory->create($options)->render();
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $sectionContainerResult);
|
||||
} else {
|
||||
// Set up options for single element
|
||||
$fakeParameterArray = [
|
||||
'fieldConf' => [
|
||||
'label' => $languageService->sL(trim($flexFormFieldArray['label'] ?? '')),
|
||||
'config' => $flexFormFieldArray['config'] ?? [],
|
||||
'children' => $flexFormFieldArray['children'] ?? [],
|
||||
// https://docs.typo3.org/m/typo3/reference-tca/main/en-us/Columns/Properties/OnChange.html
|
||||
'onChange' => $flexFormFieldArray['onChange'] ?? '',
|
||||
],
|
||||
'fieldChangeFunc' => $parameterArray['fieldChangeFunc'],
|
||||
'label' => $parameterArray['label'] ?? '',
|
||||
];
|
||||
|
||||
if (isset($flexFormFieldArray['description']) && !empty($flexFormFieldArray['description'])) {
|
||||
$fakeParameterArray['fieldConf']['description'] = $flexFormFieldArray['description'];
|
||||
}
|
||||
|
||||
if ($fakeParameterArray['fieldConf']['onChange'] === 'reload') {
|
||||
$confirmation = $this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE);
|
||||
$fakeParameterArray['fieldChangeFunc']['alert'] = new ReloadOnFieldChange($confirmation);
|
||||
}
|
||||
|
||||
$originalFieldName = $parameterArray['itemFormElName'];
|
||||
$fakeParameterArray['itemFormElName'] = $parameterArray['itemFormElName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]';
|
||||
if ($fakeParameterArray['itemFormElName'] !== $originalFieldName) {
|
||||
// If calculated itemFormElName is different from originalFieldName
|
||||
// change the originalFieldName in TBE_EDITOR_fieldChanged. This is
|
||||
// especially relevant for wizards writing their content back to hidden fields
|
||||
$onFieldChange = $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] ?? null;
|
||||
if ($onFieldChange instanceof UpdateValueOnFieldChange) {
|
||||
$fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = $onFieldChange->withElementName($fakeParameterArray['itemFormElName']);
|
||||
}
|
||||
}
|
||||
if (array_key_exists('vDEF', $flexFormRowData[$flexFormFieldName] ?? [])) {
|
||||
$fakeParameterArray['itemFormElValue'] = $flexFormRowData[$flexFormFieldName]['vDEF'];
|
||||
} else {
|
||||
$fakeParameterArray['itemFormElValue'] = $fakeParameterArray['fieldConf']['config']['default'] ?? '';
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
// Set either flexFormFieldName or flexFormContainerFieldName, depending on if we are a "regular" field or a flex container section field
|
||||
if (empty($options['flexFormFieldName'])) {
|
||||
$options['flexFormFieldName'] = $flexFormFieldName;
|
||||
} else {
|
||||
$options['flexFormContainerFieldName'] = $flexFormFieldName;
|
||||
}
|
||||
$options['parameterArray'] = $fakeParameterArray;
|
||||
$options['elementBaseName'] = $this->data['elementBaseName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]';
|
||||
|
||||
if (!empty($flexFormFieldArray['config']['renderType'])) {
|
||||
$options['renderType'] = $flexFormFieldArray['config']['renderType'];
|
||||
} else {
|
||||
// Fallback to type if no renderType is given
|
||||
$options['renderType'] = $flexFormFieldArray['config']['type'];
|
||||
}
|
||||
$childResult = $this->nodeFactory->create($options)->render();
|
||||
|
||||
if (!empty($childResult['html'])) {
|
||||
$html = [];
|
||||
$html[] = '<div class="form-section" data-id="' . htmlspecialchars($flexFormFieldName) . '">';
|
||||
$html[] = '<div class="form-group t3js-formengine-palette-field t3js-formengine-validation-marker">';
|
||||
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
|
||||
$html[] = $childResult['html'];
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$resultArray['html'] .= implode(LF, $html);
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?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\Container;
|
||||
|
||||
/**
|
||||
* Entry container to a flex form element. This container is created by
|
||||
* SingleFieldContainer if a type='flex' field is rendered.
|
||||
*
|
||||
* It either forks a FlexFormTabsContainer or a FlexFormNoTabsContainer.
|
||||
*/
|
||||
class FlexFormEntryContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$flexFormDataStructureIdentifier = $this->data['parameterArray']['fieldConf']['config']['dataStructureIdentifier'];
|
||||
$flexFormDataStructureArray = $this->data['parameterArray']['fieldConf']['config']['ds'];
|
||||
|
||||
$options = $this->data;
|
||||
$options['flexFormDataStructureIdentifier'] = $flexFormDataStructureIdentifier;
|
||||
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray;
|
||||
$options['flexFormRowData'] = $this->data['parameterArray']['itemFormElValue'];
|
||||
$options['renderType'] = 'flexFormNoTabsContainer';
|
||||
|
||||
// Enable tabs if there is more than one sheet
|
||||
if (count($flexFormDataStructureArray['sheets']) > 1) {
|
||||
$options['renderType'] = 'flexFormTabsContainer';
|
||||
}
|
||||
|
||||
$resultArray = $this->nodeFactory->create($options)->render();
|
||||
$resultArray['html'] = '<div class="panel">' . $resultArray['html'] . '</div>';
|
||||
$resultArray['html'] = $this->wrapWithFieldsetAndLegend($resultArray['html']);
|
||||
return $resultArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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\Container;
|
||||
|
||||
/**
|
||||
* Handle a flex form that has no tabs.
|
||||
*
|
||||
* This container is called by FlexFormEntryContainer if only a default sheet
|
||||
* exists. It evaluates the display condition and hands over rendering of single
|
||||
* fields to FlexFormElementContainer.
|
||||
*/
|
||||
class FlexFormNoTabsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$flexFormRowData = $this->data['flexFormRowData'];
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Determine this single sheet name, most often it ends up with sDEF, except if only one sheet was defined
|
||||
$flexFormSheetNames = array_keys($flexFormDataStructureArray['sheets']);
|
||||
$sheetName = array_pop($flexFormSheetNames);
|
||||
$flexFormRowDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?? [];
|
||||
|
||||
unset($flexFormDataStructureArray['meta']);
|
||||
|
||||
if (!is_array($flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'])) {
|
||||
$resultArray['html'] = 'Data Structure ERROR: No [\'ROOT\'][\'el\'] element found in flex form definition.';
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'];
|
||||
$options['flexFormRowData'] = $flexFormRowDataSubPart;
|
||||
$options['flexFormSheetName'] = $sheetName;
|
||||
$options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
|
||||
$options['parameterArray'] = $parameterArray;
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$resultArray['html'] = $fieldInformationResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
$options['renderType'] = 'flexFormElementContainer';
|
||||
$childResult = $this->nodeFactory->create($options)->render();
|
||||
return $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle flex form sections.
|
||||
*
|
||||
* This container is created by FlexFormElementContainer if a "single" element is in
|
||||
* fact a section. For each existing section container it creates as FlexFormContainerContainer
|
||||
* to render its inner fields.
|
||||
* Additionally, a button for each possible container is rendered with information for the
|
||||
* ajax controller that fetches one on click.
|
||||
*/
|
||||
class FlexFormSectionContainer extends AbstractContainer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly IconFactory $iconFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$flexFormRowData = $this->data['flexFormRowData'];
|
||||
$flexFormFieldName = $this->data['flexFormFieldName'];
|
||||
$flexFormSheetName = $this->data['flexFormSheetName'];
|
||||
|
||||
$userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Render each existing container
|
||||
foreach ($flexFormDataStructureArray['children'] as $flexFormContainerIdentifier => $containerDataStructure) {
|
||||
$existingContainerData = $flexFormRowData[$flexFormContainerIdentifier];
|
||||
$existingSectionContainerDataStructureType = key($existingContainerData);
|
||||
$existingContainerData = $existingContainerData[$existingSectionContainerDataStructureType];
|
||||
$options = $this->data;
|
||||
$options['flexFormRowData'] = $existingContainerData['el'];
|
||||
$options['flexFormDataStructureArray'] = $containerDataStructure;
|
||||
$options['flexFormFormPrefix'] = $this->data['flexFormFormPrefix'] . '[' . $flexFormFieldName . '][el]';
|
||||
$options['flexFormContainerName'] = $existingSectionContainerDataStructureType;
|
||||
$options['flexFormContainerIdentifier'] = $flexFormContainerIdentifier;
|
||||
$options['renderType'] = 'flexFormContainerContainer';
|
||||
$flexFormContainerContainerResult = $this->nodeFactory->create($options)->render();
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerResult);
|
||||
}
|
||||
|
||||
$containerId = sprintf('flexform-section-container-%s-%s-%s-%s', $flexFormSheetName, $this->data['fieldName'], md5($flexFormFieldName), md5($this->data['elementBaseName']));
|
||||
$sectionContainerId = sprintf('flexform-section-%s-%s-%s-%s', $flexFormSheetName, $this->data['fieldName'], md5($flexFormFieldName), md5($this->data['elementBaseName']));
|
||||
$hashedSectionContainerId = 'section-' . md5($sectionContainerId);
|
||||
|
||||
// "New container" handling: Creates buttons for each possible container with all relevant information for the ajax call.
|
||||
$containerTemplatesHtml = [];
|
||||
foreach ($flexFormDataStructureArray['el'] as $flexFormContainerName => $flexFormFieldDefinition) {
|
||||
$containerTitle = '';
|
||||
if (!empty(trim($flexFormFieldDefinition['title']))) {
|
||||
$containerTitle = $languageService->sL(trim($flexFormFieldDefinition['title']));
|
||||
}
|
||||
$containerTemplateHtml = [];
|
||||
$containerTemplateHtml[] = '<a';
|
||||
$containerTemplateHtml[] = 'href="#"';
|
||||
$containerTemplateHtml[] = 'class="btn btn-default t3js-flex-container-add"';
|
||||
$containerTemplateHtml[] = 'data-vanillauid="' . (int)$this->data['vanillaUid'] . '"';
|
||||
// no int cast for databaseRow uid, this can be "NEW1234..."
|
||||
$containerTemplateHtml[] = 'data-databaserowuid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"';
|
||||
$containerTemplateHtml[] = 'data-command="' . htmlspecialchars($this->data['command']) . '"';
|
||||
$containerTemplateHtml[] = 'data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
|
||||
$containerTemplateHtml[] = 'data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
|
||||
$containerTemplateHtml[] = 'data-recordtypevalue="' . $this->data['recordTypeValue'] . '"';
|
||||
$containerTemplateHtml[] = 'data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
|
||||
$containerTemplateHtml[] = 'data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
|
||||
$containerTemplateHtml[] = 'data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
|
||||
$containerTemplateHtml[] = 'data-target="#' . htmlspecialchars($hashedSectionContainerId) . '"';
|
||||
$containerTemplateHtml[] = '>';
|
||||
$containerTemplateHtml[] = $this->iconFactory->getIcon('actions-document-new', IconSize::SMALL)->render();
|
||||
$containerTemplateHtml[] = htmlspecialchars(GeneralUtility::fixed_lgd_cs($containerTitle, 30));
|
||||
$containerTemplateHtml[] = '</a>';
|
||||
$containerTemplatesHtml[] = implode(LF, $containerTemplateHtml);
|
||||
}
|
||||
// Create new elements links
|
||||
$createElementsHtml = [];
|
||||
if ($userHasAccessToDefaultLanguage) {
|
||||
$createElementsHtml[] = '<div class="t3-form-field-add-flexsection">';
|
||||
$createElementsHtml[] = '<div class="btn-group">';
|
||||
$createElementsHtml[] = implode('', $containerTemplatesHtml);
|
||||
$createElementsHtml[] = '</div>';
|
||||
$createElementsHtml[] = '</div>';
|
||||
}
|
||||
|
||||
$sectionTitle = '';
|
||||
if (!empty(trim($flexFormDataStructureArray['title'] ?? ''))) {
|
||||
$sectionTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
|
||||
}
|
||||
|
||||
// Wrap child stuff
|
||||
$toggleAll = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall'));
|
||||
$html = [];
|
||||
$html[] = '<div class="form-section">';
|
||||
$html[] = '<div class="t3-form-field-container t3-form-flex" id="' . htmlspecialchars($containerId) . '" data-section="#' . htmlspecialchars($hashedSectionContainerId) . '">';
|
||||
$html[] = '<fieldset>';
|
||||
$html[] = '<legend class="form-label t3js-formengine-label">';
|
||||
$html[] = htmlspecialchars($sectionTitle);
|
||||
$html[] = '</legend>';
|
||||
$html[] = '<div class="form-group">';
|
||||
$html[] = '<button class="btn btn-default t3-form-flexsection-toggle" type="button" title="' . $toggleAll . '">';
|
||||
$html[] = $this->iconFactory->getIcon('actions-move-right', IconSize::SMALL)->render() . $toggleAll;
|
||||
$html[] = '</button>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div';
|
||||
$html[] = 'id="' . htmlspecialchars($hashedSectionContainerId) . '"';
|
||||
$html[] = 'class="panel-group panel-hover t3-form-field-container-flexsection t3-flex-container"';
|
||||
$html[] = 'data-t3-flex-allow-restructure="' . ($userHasAccessToDefaultLanguage ? '1' : '0') . '"';
|
||||
$html[] = '>';
|
||||
$html[] = $resultArray['html'];
|
||||
$html[] = '</div>';
|
||||
$html[] = implode(LF, $createElementsHtml);
|
||||
$html[] = '</fieldset>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
|
||||
$resultArray['html'] = implode(LF, $html);
|
||||
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
|
||||
'@typo3/backend/form-engine/container/flex-form-section-container.js'
|
||||
)->instance($containerId);
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
|
||||
/**
|
||||
* Handle flex forms that have tabs (multiple "sheets").
|
||||
*
|
||||
* This container is called by FlexFormEntryContainer. It resolves each
|
||||
* sheet and hands rendering of single sheet content over to FlexFormElementContainer.
|
||||
*/
|
||||
class FlexFormTabsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$flexFormRowData = $this->data['flexFormRowData'];
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/tab.js');
|
||||
|
||||
$domIdPrefix = 'DTM-' . md5($this->data['parameterArray']['itemFormElName']);
|
||||
$tabCounter = 0;
|
||||
$tabElements = [];
|
||||
foreach ($flexFormDataStructureArray['sheets'] as $sheetName => $sheetDataStructure) {
|
||||
$flexFormRowSheetDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?? [];
|
||||
|
||||
if (!is_array($sheetDataStructure['ROOT']['el'])) {
|
||||
$resultArray['html'] .= LF . 'No Data Structure ERROR: No [\'ROOT\'][\'el\'] found for sheet "' . $sheetName . '".';
|
||||
continue;
|
||||
}
|
||||
|
||||
$tabCounter++;
|
||||
|
||||
$options = $this->data;
|
||||
$options['flexFormDataStructureArray'] = $sheetDataStructure['ROOT']['el'];
|
||||
$options['flexFormRowData'] = $flexFormRowSheetDataSubPart;
|
||||
$options['flexFormSheetName'] = $sheetName;
|
||||
$options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
|
||||
$options['parameterArray'] = $parameterArray;
|
||||
// Merge elements of this tab into a single list again and hand over to
|
||||
// palette and single field container to render this group
|
||||
$options['tabAndInlineStack'][] = [
|
||||
'tab',
|
||||
$domIdPrefix . '-' . $tabCounter,
|
||||
];
|
||||
$options['renderType'] = 'flexFormElementContainer';
|
||||
$childReturn = $this->nodeFactory->create($options)->render();
|
||||
|
||||
if ($childReturn['html'] !== '') {
|
||||
$tabElements[] = [
|
||||
'label' => !empty(trim($sheetDataStructure['ROOT']['sheetTitle'] ?? '')) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetTitle'])) : $sheetName,
|
||||
'content' => $childReturn['html'],
|
||||
'description' => trim($sheetDataStructure['ROOT']['sheetDescription'] ?? '') ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetDescription'])) : '',
|
||||
'linkTitle' => trim($sheetDataStructure['ROOT']['sheetShortDescr'] ?? '') ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetShortDescr'])) : '',
|
||||
];
|
||||
}
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childReturn, false);
|
||||
}
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$resultArray['html'] = $fieldInformationResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
$resultArray['html'] .= $this->renderTabMenu($tabElements, $domIdPrefix);
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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\Container;
|
||||
|
||||
/**
|
||||
* Entry container called from controllers.
|
||||
* It either calls a FullRecordContainer or ListOfFieldsContainer to render
|
||||
* a full record or only some fields from a full record.
|
||||
*/
|
||||
class FormWrapContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$options = $this->data;
|
||||
if (empty($this->data['fieldListToRender'])) {
|
||||
$options['renderType'] = 'fullRecordContainer';
|
||||
} else {
|
||||
$options['renderType'] = 'listOfFieldsContainer';
|
||||
}
|
||||
$result = $this->nodeFactory->create($options)->render();
|
||||
|
||||
$childHtml = $result['html'];
|
||||
|
||||
$view = $this->backendViewFactory->create($this->data['request']);
|
||||
|
||||
$descriptionColumn = !empty($this->data['processedTca']['ctrl']['descriptionColumn'])
|
||||
? $this->data['processedTca']['ctrl']['descriptionColumn'] : null;
|
||||
if ($descriptionColumn !== null && isset($this->data['databaseRow'][$descriptionColumn])) {
|
||||
$view->assign('recordDescription', $this->data['databaseRow'][$descriptionColumn]);
|
||||
}
|
||||
$readOnlyRecord = !empty($this->data['processedTca']['ctrl']['readOnly'])
|
||||
? (bool)$this->data['processedTca']['ctrl']['readOnly'] : null;
|
||||
if ($readOnlyRecord === true) {
|
||||
$view->assign('recordReadonly', true);
|
||||
}
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$fieldInformationHtml = $fieldInformationResult['html'];
|
||||
$result = $this->mergeChildReturnIntoExistingResult($result, $fieldInformationResult, false);
|
||||
|
||||
$fieldWizardResult = $this->renderFieldWizard();
|
||||
$fieldWizardHtml = $fieldWizardResult['html'];
|
||||
$result = $this->mergeChildReturnIntoExistingResult($result, $fieldWizardResult, false);
|
||||
|
||||
$view->assignMultiple([
|
||||
'fieldInformationHtml' => $fieldInformationHtml,
|
||||
'fieldWizardHtml' => $fieldWizardHtml,
|
||||
'childHtml' => $childHtml,
|
||||
'isNewRecord' => $this->data['command'] === 'new',
|
||||
]);
|
||||
$result['html'] = $view->render('Form/FormWrapContainer');
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Exception\NoFieldsToRenderException;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A container rendering a "full record". This is an entry container used as first
|
||||
* step into the rendering tree..
|
||||
*
|
||||
* This container determines the to be rendered fields depending on the record type,
|
||||
* initializes possible language base data, finds out if tabs should be rendered and
|
||||
* then calls either TabsContainer or a NoTabsContainer for further processing.
|
||||
*/
|
||||
class FullRecordContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$recordTypeValue = $this->data['recordTypeValue'];
|
||||
|
||||
// List of items to be rendered
|
||||
$itemList = $this->data['processedTca']['types'][$recordTypeValue]['showitem'];
|
||||
|
||||
$fieldsArray = GeneralUtility::trimExplode(',', $itemList, true);
|
||||
|
||||
if ($fieldsArray === []) {
|
||||
throw new NoFieldsToRenderException('No fields defined for record type "' . $recordTypeValue . '" of table "' . $this->data['tableName'] . '"', 1730106227);
|
||||
}
|
||||
|
||||
// Streamline the fields array
|
||||
// First, make sure there is always a --div-- definition for the first element
|
||||
if (!str_starts_with($fieldsArray[0], '--div--')) {
|
||||
array_unshift($fieldsArray, '--div--;core.form.tabs:general');
|
||||
}
|
||||
// If first tab has no label definition, add "general" label
|
||||
$firstTabHasLabel = count(GeneralUtility::trimExplode(';', $fieldsArray[0])) > 1;
|
||||
if (!$firstTabHasLabel) {
|
||||
$fieldsArray[0] = '--div--;core.form.tabs:general';
|
||||
}
|
||||
// If there are at least two --div-- definitions, inner container will be a TabContainer, else a NoTabContainer
|
||||
$tabCount = 0;
|
||||
foreach ($fieldsArray as $field) {
|
||||
if (str_starts_with($field, '--div--')) {
|
||||
$tabCount++;
|
||||
}
|
||||
}
|
||||
$hasTabs = true;
|
||||
if ($tabCount < 2) {
|
||||
// Remove first tab definition again if there is only one tab defined
|
||||
array_shift($fieldsArray);
|
||||
$hasTabs = false;
|
||||
}
|
||||
|
||||
$data = $this->data;
|
||||
$data['fieldsArray'] = $fieldsArray;
|
||||
if ($hasTabs) {
|
||||
$data['renderType'] = 'tabsContainer';
|
||||
} else {
|
||||
$data['renderType'] = 'noTabsContainer';
|
||||
}
|
||||
|
||||
return $this->nodeFactory->create($data)->render();
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Inline element entry container.
|
||||
*
|
||||
* This container is the entry step to rendering an inline element. It is created by SingleFieldContainer.
|
||||
*
|
||||
* The code creates the main structure for the single inline elements, initializes
|
||||
* the inlineData array, that is manipulated and also returned back in its manipulated state.
|
||||
* The "control" stuff of inline elements is rendered here, for example the "create new" button.
|
||||
*
|
||||
* For each existing inline relation an InlineRecordContainer is called for further processing.
|
||||
*/
|
||||
class InlineControlContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Inline data array used in JS, returned as JSON object to frontend
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $inlineData = [];
|
||||
|
||||
/**
|
||||
* @var array<int,JavaScriptModuleInstruction>
|
||||
*/
|
||||
protected $javaScriptModules = [];
|
||||
|
||||
/**
|
||||
* @var array Default wizards
|
||||
*/
|
||||
protected $defaultFieldWizard = [
|
||||
'localizationStateSelector' => [
|
||||
'renderType' => 'localizationStateSelector',
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly IconFactory $iconFactory,
|
||||
private readonly InlineStackProcessor $inlineStackProcessor,
|
||||
private readonly HashService $hashService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$this->inlineData = $this->data['inlineData'];
|
||||
|
||||
$inlineStructure = $this->data['inlineStructure'];
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$field = $this->data['fieldName'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$config = $parameterArray['fieldConf']['config'];
|
||||
$foreign_table = $config['foreign_table'];
|
||||
$isReadOnly = isset($config['readOnly']) && $config['readOnly'];
|
||||
$language = 0;
|
||||
if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) {
|
||||
$languageFieldName = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getLanguageField()->getName();
|
||||
$language = isset($row[$languageFieldName][0]) ? (int)$row[$languageFieldName][0] : (int)($row[$languageFieldName] ?? 0);
|
||||
}
|
||||
|
||||
// Add the current inline job to the structure stack
|
||||
$newStructureItem = [
|
||||
'table' => $table,
|
||||
'uid' => $row['uid'],
|
||||
'field' => $field,
|
||||
'config' => $config,
|
||||
];
|
||||
// Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
|
||||
if (!empty($parameterArray['itemFormElName'])) {
|
||||
$flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
|
||||
if ($flexFormParts !== null) {
|
||||
$newStructureItem['flexform'] = $flexFormParts;
|
||||
}
|
||||
}
|
||||
$inlineStructure['stable'][] = $newStructureItem;
|
||||
|
||||
// Transport the flexform DS identifier fields to the FormInlineAjaxController
|
||||
if (!empty($newStructureItem['flexform'])
|
||||
&& isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'])
|
||||
) {
|
||||
$config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
|
||||
}
|
||||
|
||||
// Hand over original returnUrl to FormInlineAjaxController. Needed if opening for instance a
|
||||
// nested element in a new view to then go back to the original returnUrl and not the url of
|
||||
// the inline ajax controller
|
||||
$config['originalReturnUrl'] = $this->data['returnUrl'];
|
||||
|
||||
// e.g. data[<table>][<uid>][<field>]
|
||||
$nameForm = $this->inlineStackProcessor->getFormPrefixFromStructure($inlineStructure);
|
||||
// e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
|
||||
$nameObject = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']);
|
||||
|
||||
$inlineChildren = $parameterArray['fieldConf']['children'] ?? [];
|
||||
|
||||
$config['inline']['first'] = $config['inline']['last'] = false;
|
||||
if (is_array($inlineChildren) && $inlineChildren !== []) {
|
||||
$firstChild = array_first($inlineChildren);
|
||||
if (isset($firstChild['databaseRow']['uid'])) {
|
||||
$config['inline']['first'] = $firstChild['databaseRow']['uid'];
|
||||
}
|
||||
$lastChild = array_last($inlineChildren);
|
||||
if (isset($lastChild['databaseRow']['uid'])) {
|
||||
$config['inline']['last'] = $lastChild['databaseRow']['uid'];
|
||||
}
|
||||
}
|
||||
|
||||
$top = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0);
|
||||
|
||||
$this->inlineData['config'][$nameObject] = [
|
||||
'table' => $foreign_table,
|
||||
];
|
||||
$configJson = (string)json_encode($config);
|
||||
$this->inlineData['config'][$nameObject . '-' . $foreign_table] = [
|
||||
'top' => [
|
||||
'table' => $top['table'],
|
||||
'uid' => $top['uid'],
|
||||
],
|
||||
'context' => [
|
||||
'config' => $configJson,
|
||||
'hmac' => $this->hashService->hmac($configJson, 'InlineContext'),
|
||||
],
|
||||
];
|
||||
$this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
|
||||
|
||||
$uniqueMax = 0;
|
||||
$uniqueIds = [];
|
||||
|
||||
if ($config['foreign_unique'] ?? false) {
|
||||
// Add inlineData['unique'] with JS unique configuration
|
||||
// @todo: Improve validation and throw an exception if type is neither select nor group here
|
||||
$type = ($config['selectorOrUniqueConfiguration']['config']['type'] ?? '') === 'select' ? 'select' : 'groupdb';
|
||||
foreach ($inlineChildren as $child) {
|
||||
// Determine used unique ids, skip not localized records
|
||||
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$value = $child['databaseRow'][$config['foreign_unique']];
|
||||
// We're assuming there is only one connected value here for both select and group
|
||||
if ($type === 'select') {
|
||||
// A select field is an array of uids. See TcaSelectItems data provider for details.
|
||||
// Pick first entry, ends up as eg. $value = 42.
|
||||
$value = $value['0'] ?? [];
|
||||
} else {
|
||||
// A group field is an array of arrays containing uid + table + title + row.
|
||||
// See TcaGroup data provider for details.
|
||||
// Pick the first one (always on 0), and use uid + table only. Exclude title + row
|
||||
// since the entire inlineData['unique'] array ends up in JavaScript in the end
|
||||
// and we don't need and want the title and the entire row data in the frontend.
|
||||
// Ends up as $value = [ 'uid' => '42', 'table' => 'tx_my_table' ]
|
||||
$value = [
|
||||
'uid' => $value[0]['uid'],
|
||||
'table' => $value[0]['table'],
|
||||
];
|
||||
}
|
||||
// Note structure of $value is different in select vs. group: It's a uid for select, but an
|
||||
// array with uid + table for group.
|
||||
if (isset($child['databaseRow']['uid'])) {
|
||||
$uniqueIds[$child['databaseRow']['uid']] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
$possibleRecords = $config['selectorOrUniquePossibleRecords'] ?? [];
|
||||
$possibleRecordsUidToTitle = [];
|
||||
foreach ($possibleRecords as $possibleRecord) {
|
||||
$possibleRecordsUidToTitle[$possibleRecord['value']] = $possibleRecord['label'];
|
||||
}
|
||||
$uniqueMax = ($config['appearance']['useCombination'] ?? false) || empty($possibleRecords) ? -1 : count($possibleRecords);
|
||||
$this->inlineData['unique'][$nameObject . '-' . $foreign_table] = [
|
||||
'max' => $uniqueMax,
|
||||
'used' => $uniqueIds,
|
||||
'type' => $type,
|
||||
'table' => $foreign_table,
|
||||
'elTable' => $config['selectorOrUniqueConfiguration']['foreignTable'] ?? '',
|
||||
'field' => $config['foreign_unique'] ?? '',
|
||||
'selector' => ($config['selectorOrUniqueConfiguration']['isSelector'] ?? false) ? $type : false,
|
||||
'possible' => $possibleRecordsUidToTitle,
|
||||
];
|
||||
}
|
||||
|
||||
$resultArray['inlineData'] = $this->inlineData;
|
||||
|
||||
// @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider
|
||||
$uidOfDefaultRecord = 0;
|
||||
if ($this->data['tcaSchemata']->has($table) && $this->data['tcaSchemata']->get($table)->hasCapability(TcaSchemaCapability::Language)) {
|
||||
$originPointerField = $this->data['tcaSchemata']->get($table)->getCapability(TcaSchemaCapability::Language)->getTranslationOriginPointerField()->getName();
|
||||
$uidOfDefaultRecord = $row[$originPointerField] ?? 0;
|
||||
}
|
||||
$isLocalizedParent = $language > 0
|
||||
&& ($uidOfDefaultRecord[0] ?? $uidOfDefaultRecord) > 0
|
||||
&& MathUtility::canBeInterpretedAsInteger($row['uid']);
|
||||
$numberOfFullLocalizedChildren = 0;
|
||||
$numberOfNotYetLocalizedChildren = 0;
|
||||
foreach ($inlineChildren as $child) {
|
||||
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$numberOfFullLocalizedChildren++;
|
||||
}
|
||||
if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$numberOfNotYetLocalizedChildren++;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the localization buttons if needed
|
||||
$localizationButtons = '';
|
||||
if ($numberOfNotYetLocalizedChildren) {
|
||||
// Add the "Localize all records" button before all child records:
|
||||
if (!empty($config['appearance']['showAllLocalizationLink'])) {
|
||||
$localizationButtons = ' ' . $this->getLevelInteractionButton('localize', $config);
|
||||
}
|
||||
// Add the "Synchronize with default language" button before all child records:
|
||||
if (!empty($config['appearance']['showSynchronizationLink'])) {
|
||||
$localizationButtons .= ' ' . $this->getLevelInteractionButton('synchronize', $config);
|
||||
}
|
||||
}
|
||||
|
||||
// Hide the "Create new record" button if there are more than maxitems or the field is read-only
|
||||
if ($isReadOnly || $numberOfFullLocalizedChildren >= ($config['maxitems'] ?? 0) || ($uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax)) {
|
||||
$config['inline']['hideNewButton'] = true;
|
||||
}
|
||||
|
||||
// Render the "new record" level button:
|
||||
$newRecordButton = '';
|
||||
// For b/w compatibility, "showNewRecordLink" - in contrast to the other show* options - defaults to TRUE
|
||||
if (!isset($config['appearance']['showNewRecordLink']) || $config['appearance']['showNewRecordLink']) {
|
||||
$newRecordButton = $this->getLevelInteractionButton('newRecord', $config);
|
||||
}
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$html = $fieldInformationResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
// Wrap all inline fields of a record with a custom element (container)
|
||||
$formGroupAttributes = [
|
||||
'id' => $nameObject,
|
||||
'data-type' => 'record',
|
||||
'data-object-group' => $nameObject . '-' . $foreign_table,
|
||||
'data-form-field' => $nameForm,
|
||||
'data-expand-single' => (bool)($config['appearance']['expandSingle'] ?? false) ? 'true' : 'false',
|
||||
'data-sortable' => (bool)($config['appearance']['useSortable'] ?? false) ? 'true' : 'false',
|
||||
'data-min' => (int)($config['minitems'] ?? 0),
|
||||
'data-max' => (int)($config['maxitems'] ?? 0),
|
||||
];
|
||||
$html .= '<typo3-formengine-container-inline ' . GeneralUtility::implodeAttributes($formGroupAttributes, true) . '>';
|
||||
|
||||
// Add the level buttons before all child records:
|
||||
if (in_array($config['appearance']['levelLinksPosition'], ['both', 'top'], true)) {
|
||||
$html .= '<div class="form-group t3js-formengine-validation-marker t3js-inline-controls">' . $newRecordButton . $localizationButtons . '</div>';
|
||||
}
|
||||
|
||||
// If it's required to select from possible child records (reusable children), add a selector box
|
||||
if (!$isReadOnly && ($config['foreign_selector'] ?? false) && ($config['appearance']['showPossibleRecordsSelector'] ?? true) !== false) {
|
||||
if (($config['selectorOrUniqueConfiguration']['config']['type'] ?? false) === 'select') {
|
||||
$selectorBox = $this->renderPossibleRecordsSelectorTypeSelect($inlineStructure, $config, $uniqueIds);
|
||||
} else {
|
||||
$selectorBox = $this->renderPossibleRecordsSelectorTypeGroupDB($inlineStructure, $config);
|
||||
}
|
||||
$html .= $selectorBox . $localizationButtons;
|
||||
}
|
||||
|
||||
$title = $languageService->sL(trim($parameterArray['fieldConf']['label'] ?? ''));
|
||||
$html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
|
||||
|
||||
$sortableRecordUids = [];
|
||||
foreach ($inlineChildren as $options) {
|
||||
$options['inlineParentUid'] = $row['uid'];
|
||||
$options['inlineFirstPid'] = $this->data['inlineFirstPid'];
|
||||
// @todo: this can be removed if this container no longer sets additional info to $config
|
||||
$options['inlineParentConfig'] = $config;
|
||||
$options['inlineData'] = $this->inlineData;
|
||||
$options['inlineStructure'] = $inlineStructure;
|
||||
$options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
|
||||
$options['renderType'] = 'inlineRecordContainer';
|
||||
$childResult = $this->nodeFactory->create($options)->render();
|
||||
$html .= $childResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
|
||||
if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext'] && isset($options['databaseRow']['uid'])) {
|
||||
// Don't add record to list of "valid" uids if it is only the default
|
||||
// language record of a not yet localized child
|
||||
$sortableRecordUids[] = $options['databaseRow']['uid'];
|
||||
}
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
$fieldWizardResult = $this->renderFieldWizard();
|
||||
$fieldWizardHtml = $fieldWizardResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
|
||||
$html .= $fieldWizardHtml;
|
||||
|
||||
// Add the level buttons after all child records:
|
||||
if (in_array($config['appearance']['levelLinksPosition'], ['both', 'bottom'], true)) {
|
||||
$html .= '<div class="form-group t3js-formengine-validation-marker t3js-inline-controls">' . $newRecordButton . $localizationButtons . '</div>';
|
||||
}
|
||||
if (is_array($config['customControls'] ?? false)) {
|
||||
$html .= '<div id="' . $nameObject . '_customControls">';
|
||||
foreach ($config['customControls'] as $customControlConfig) {
|
||||
if (!isset($customControlConfig['userFunc'])) {
|
||||
throw new \RuntimeException('Support for customControl without a userFunc key in TCA type inline is not supported.', 1548052629);
|
||||
}
|
||||
$parameters = [
|
||||
'table' => $table,
|
||||
'field' => $field,
|
||||
'row' => $row,
|
||||
'nameObject' => $nameObject,
|
||||
'nameForm' => $nameForm,
|
||||
'config' => $config,
|
||||
'customControlConfig' => $customControlConfig,
|
||||
// Warning: By reference should be used with care here and exists mostly to allow additional $resultArray['javaScriptModules']
|
||||
'resultArray' => &$resultArray,
|
||||
];
|
||||
$html .= GeneralUtility::callUserFunction($customControlConfig['userFunc'], $parameters, $this);
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
$resultArray['javaScriptModules'] = array_merge($resultArray['javaScriptModules'], $this->javaScriptModules);
|
||||
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create(
|
||||
'@typo3/backend/form-engine/container/inline-control-container.js'
|
||||
);
|
||||
|
||||
// Publish the uids of the child records in the given order to the browser
|
||||
$html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
|
||||
. ' data-formengine-validation-rules="'
|
||||
. htmlspecialchars($this->getValidationDataAsJsonString([
|
||||
'type' => 'inline',
|
||||
'minitems' => $config['minitems'] ?? null,
|
||||
'maxitems' => $config['maxitems'] ?? null,
|
||||
]))
|
||||
. '"'
|
||||
. ' class="inlineRecord" />';
|
||||
// Close the wrap for all inline fields (container)
|
||||
$html .= '</typo3-formengine-container-inline>';
|
||||
|
||||
$resultArray['html'] = $this->wrapWithFieldsetAndLegend($html);
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the HTML code of a general button to be used on a level of inline children.
|
||||
* The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
|
||||
*
|
||||
* @param string $type The button type, values are 'newRecord', 'localize' and 'synchronize'.
|
||||
* @param array $conf TCA configuration of the parent(!) field
|
||||
* @return string The HTML code of the new button, wrapped in a div
|
||||
*/
|
||||
protected function getLevelInteractionButton(string $type, array $conf = []): string
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$attributes = [];
|
||||
switch ($type) {
|
||||
case 'newRecord':
|
||||
$title = htmlspecialchars($languageService->sL('core.core:cm.createnew'));
|
||||
$icon = 'actions-plus';
|
||||
$attributes['class'] = 'btn btn-default t3js-create-new-button';
|
||||
$attributes['data-type'] = 'newRecord';
|
||||
if (!empty($conf['inline']['hideNewButton'])) {
|
||||
$attributes['hidden'] = 'hidden';
|
||||
}
|
||||
if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
|
||||
$title = htmlspecialchars(sprintf(
|
||||
$languageService->sL('core.core:cm.createnew.link'),
|
||||
$languageService->sL($this->data['tcaSchemata']->get($conf['foreign_table'])->getTitle()),
|
||||
));
|
||||
} elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
|
||||
$title = htmlspecialchars($languageService->sL($conf['appearance']['newRecordLinkTitle']));
|
||||
}
|
||||
break;
|
||||
case 'localize':
|
||||
$title = htmlspecialchars($languageService->sL('core.misc:localizeAllRecords'));
|
||||
$icon = 'actions-document-localize';
|
||||
$attributes['class'] = 'btn btn-default t3js-synchronizelocalize-button';
|
||||
$attributes['data-type'] = 'localize';
|
||||
break;
|
||||
case 'synchronize':
|
||||
$title = htmlspecialchars($languageService->sL('core.misc:synchronizeWithOriginalLanguage'));
|
||||
$icon = 'actions-document-synchronize';
|
||||
$attributes['class'] = 'btn btn-default t3js-synchronizelocalize-button';
|
||||
$attributes['data-type'] = 'synchronize';
|
||||
break;
|
||||
default:
|
||||
$title = '';
|
||||
$icon = '';
|
||||
}
|
||||
// Create the button:
|
||||
$icon = $icon ? $this->iconFactory->getIcon($icon, IconSize::SMALL)->render() : '';
|
||||
$attributes['title'] = $title;
|
||||
return '
|
||||
<button type="button" ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>
|
||||
' . $icon . ' ' . $title . '
|
||||
</button>
|
||||
';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a button that opens an element browser in a new window.
|
||||
* For group/db there is no way to use a "selector" like a <select>|</select>-box.
|
||||
*
|
||||
* @param array $inlineConfiguration TCA inline configuration of the parent(!) field
|
||||
* @return string A HTML button that opens an element browser in a new window
|
||||
*/
|
||||
protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineStructure, array $inlineConfiguration): string
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$groupFieldConfiguration = $inlineConfiguration['selectorOrUniqueConfiguration']['config'];
|
||||
$objectPrefix = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']) . '-' . $inlineConfiguration['foreign_table'];
|
||||
$elementBrowserEnabled = (bool)($inlineConfiguration['appearance']['elementBrowserEnabled'] ?? true);
|
||||
// Remove any white-spaces from the allowed extension lists
|
||||
$allowed = GeneralUtility::trimExplode(',', (string)($groupFieldConfiguration['allowed'] ?? ''), true);
|
||||
$item = '';
|
||||
if ($elementBrowserEnabled) {
|
||||
if (!empty($inlineConfiguration['appearance']['createNewRelationLinkTitle'])) {
|
||||
$createNewRelationText = htmlspecialchars($languageService->sL($inlineConfiguration['appearance']['createNewRelationLinkTitle']));
|
||||
} else {
|
||||
$createNewRelationText = htmlspecialchars($languageService->sL('core.core:cm.createNewRelation'));
|
||||
}
|
||||
$item .= '
|
||||
<button type="button" class="btn btn-default t3js-element-browser" data-mode="db"
|
||||
data-allowed-types="' . htmlspecialchars(implode(',', $allowed)) . '"
|
||||
data-irre-object-id="' . htmlspecialchars($objectPrefix) . '"
|
||||
title="' . $createNewRelationText . '">
|
||||
' . $this->iconFactory->getIcon('actions-insert-record', IconSize::SMALL)->render() . '
|
||||
' . $createNewRelationText . '
|
||||
</button>';
|
||||
}
|
||||
$item = '<div class="form-control-wrap t3js-inline-controls">' . $item . '</div>';
|
||||
if (!empty($allowed)) {
|
||||
$item .= '
|
||||
<div class="form-text mt-2">
|
||||
' . htmlspecialchars($languageService->sL('core.core:cm.allowedRelations')) . '
|
||||
<ul class="badge-list">
|
||||
' . implode(' ', array_map(static fn(string $item): string => '<li><span class="badge badge-secondary">' . strtoupper($item) . '</span></li>', $allowed)) . '
|
||||
</ul>
|
||||
</div>';
|
||||
}
|
||||
return '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a selector as used for the select type, to select from all available
|
||||
* records and to create a relation to the embedding record (e.g. like MM).
|
||||
*
|
||||
* @param array $config TCA inline configuration of the parent(!) field
|
||||
* @param array $uniqueIds The uids that have already been used and should be unique
|
||||
* @return string A HTML <select> box with all possible records
|
||||
*/
|
||||
protected function renderPossibleRecordsSelectorTypeSelect(array $inlineStructure, array $config, array $uniqueIds)
|
||||
{
|
||||
$config += [
|
||||
'autoSizeMax' => 0,
|
||||
'foreign_table' => '',
|
||||
];
|
||||
$possibleRecords = $config['selectorOrUniquePossibleRecords'];
|
||||
$nameObject = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']);
|
||||
// Create option tags:
|
||||
$opt = [];
|
||||
foreach ($possibleRecords as $possibleRecord) {
|
||||
if (!in_array($possibleRecord['value'], $uniqueIds)) {
|
||||
$opt[] = '<option value="' . htmlspecialchars($possibleRecord['value']) . '">' . htmlspecialchars($possibleRecord['label']) . '</option>';
|
||||
}
|
||||
}
|
||||
// Put together the selector box:
|
||||
$size = (int)($config['size'] ?? 0);
|
||||
$autoSizeMax = (int)($config['autoSizeMax'] ?? 0);
|
||||
if ($autoSizeMax > 0) {
|
||||
$size = MathUtility::forceIntegerInRange($size, 1);
|
||||
$size = MathUtility::forceIntegerInRange(count($possibleRecords) + 1, $size, $autoSizeMax);
|
||||
}
|
||||
|
||||
$item = '
|
||||
<select id="' . $nameObject . '-' . $config['foreign_table'] . '_selector" class="form-select t3js-create-new-selector"' . ($size ? ' size="' . $size . '"' : '') . '>
|
||||
' . implode('', $opt) . '
|
||||
</select>';
|
||||
|
||||
if ($size <= 1) {
|
||||
// Add a "Create new relation" button for adding new relations
|
||||
// This is necessary, if the size of the selector is "1" or if
|
||||
// there is only one record item in the select-box, that is selected by default
|
||||
// The selector-box creates a new relation on using an onChange event (see some line above)
|
||||
if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
|
||||
$createNewRelationText = htmlspecialchars($this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle']));
|
||||
} else {
|
||||
$createNewRelationText = htmlspecialchars($this->getLanguageService()->sL('core.core:cm.createNewRelation'));
|
||||
}
|
||||
$item .= '
|
||||
<button type="button" class="btn btn-default t3js-create-new-button" title="' . $createNewRelationText . '">
|
||||
' . $this->iconFactory->getIcon('actions-plus', IconSize::SMALL)->render() . $createNewRelationText . '
|
||||
</button>';
|
||||
}
|
||||
|
||||
// Wrap the selector and add a spacer to the bottom
|
||||
$item = '<div class="input-group form-group t3js-formengine-validation-marker t3js-inline-controls">' . $item . '</div>';
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts FlexForm parts of a form element name like
|
||||
* data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
|
||||
* Helper method used in inline
|
||||
*
|
||||
* @param string $formElementName The form element name
|
||||
* @return array|null
|
||||
*/
|
||||
protected function extractFlexFormParts($formElementName)
|
||||
{
|
||||
$flexFormParts = null;
|
||||
$matches = [];
|
||||
if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
|
||||
$flexFormParts = GeneralUtility::trimExplode(
|
||||
'][',
|
||||
trim($matches[1], '[]')
|
||||
);
|
||||
}
|
||||
return $flexFormParts;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,523 @@
|
||||
<?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\Container;
|
||||
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Backend\Form\Event\ModifyInlineElementControlsEvent;
|
||||
use TYPO3\CMS\Backend\Form\Event\ModifyInlineElementEnabledControlsEvent;
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Schema\Capability\TcaSchemaCapability;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Render a single inline record relation.
|
||||
*
|
||||
* This container is called by InlineControlContainer to render single existing records.
|
||||
* Furthermore, it is called by FormEngine for an incoming ajax request to expand an existing record
|
||||
* or to create a new one.
|
||||
*
|
||||
* This container creates the outer HTML of single inline records - eg. drag and drop and delete buttons.
|
||||
* For rendering of the record itself processing is handed over to FullRecordContainer.
|
||||
*/
|
||||
class InlineRecordContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Inline data array used for JSON output
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $inlineData = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly IconFactory $iconFactory,
|
||||
private readonly InlineStackProcessor $inlineStackProcessor,
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$data = $this->data;
|
||||
$this->inlineData = $data['inlineData'];
|
||||
|
||||
$record = $data['databaseRow'];
|
||||
$inlineConfig = $data['inlineParentConfig'];
|
||||
$foreignTable = $inlineConfig['foreign_table'];
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Send a mapping information to the browser via JSON:
|
||||
// e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
|
||||
$formPrefix = $this->inlineStackProcessor->getFormPrefixFromStructure($data['inlineStructure']);
|
||||
$domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($data['inlineStructure'], $data['inlineFirstPid']);
|
||||
$this->inlineData['map'][$formPrefix] = $domObjectId;
|
||||
|
||||
$resultArray['inlineData'] = $this->inlineData;
|
||||
|
||||
// Get the current naming scheme for DOM name/id attributes:
|
||||
$appendFormFieldNames = '[' . $foreignTable . '][' . ($record['uid'] ?? 0) . ']';
|
||||
$objectId = $domObjectId . '-' . $foreignTable . '-' . ($record['uid'] ?? 0);
|
||||
$classes = [];
|
||||
$html = '';
|
||||
$combinationHtml = '';
|
||||
$isNewRecord = $data['command'] === 'new';
|
||||
$hiddenField = '';
|
||||
if (isset($data['processedTca']['ctrl']['enablecolumns']['disabled'])) {
|
||||
$hiddenField = $data['processedTca']['ctrl']['enablecolumns']['disabled'];
|
||||
}
|
||||
if (!$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
if ($isNewRecord || $data['isInlineChildExpanded']) {
|
||||
// Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed
|
||||
if (isset($data['combinationChild'])) {
|
||||
$combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames);
|
||||
$combinationHtml = $combinationChild['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild, false);
|
||||
}
|
||||
$childArray = $this->renderChild($data);
|
||||
$html = $childArray['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
|
||||
} else {
|
||||
// This class is the marker for the JS-function to check if the full content has already been loaded
|
||||
$classes[] = 't3js-not-loaded';
|
||||
}
|
||||
if ($isNewRecord) {
|
||||
// Add pid of record as hidden field
|
||||
$html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[pid]" value="' . htmlspecialchars($record['pid']) . '"/>';
|
||||
// Tell DataHandler this record is expanded
|
||||
$ucFieldName = 'uc[inlineView]'
|
||||
. '[' . $data['inlineTopMostParentTableName'] . ']'
|
||||
. '[' . $data['inlineTopMostParentUid'] . ']'
|
||||
. $appendFormFieldNames;
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
|
||||
. '" value="' . (int)$data['isInlineChildExpanded'] . '" />';
|
||||
} else {
|
||||
// Set additional field for processing for saving
|
||||
$html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[delete]" value="1" disabled="disabled" />';
|
||||
if (!empty($hiddenField) && (!$data['isInlineChildExpanded'] || !in_array($hiddenField, $data['columnsToProcess'], true))) {
|
||||
$checked = !empty($record[$hiddenField]) ? ' checked="checked"' : '';
|
||||
$html .= '<input type="checkbox" class="d-none" data-formengine-input-name="data'
|
||||
. htmlspecialchars($appendFormFieldNames)
|
||||
. '[' . htmlspecialchars($hiddenField) . ']" value="1"' . $checked . ' />';
|
||||
$html .= '<input type="input" class="d-none" name="data' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[' . htmlspecialchars($hiddenField) . ']" value="' . htmlspecialchars($record[$hiddenField]) . '" />';
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($inlineConfig['renderFieldsOnly'] ?? false) {
|
||||
// Render "body" part only
|
||||
$html .= $combinationHtml;
|
||||
} else {
|
||||
// Render header row and content (if expanded)
|
||||
if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$classes[] = 'panel-placeholder';
|
||||
}
|
||||
if (!empty($hiddenField) && isset($record[$hiddenField]) && (int)$record[$hiddenField]) {
|
||||
$classes[] = 'panel-hidden';
|
||||
}
|
||||
if ($isNewRecord) {
|
||||
$classes[] = 'inlineIsNewRecord';
|
||||
}
|
||||
|
||||
$originalUniqueValue = '';
|
||||
if (isset($record['uid'], $data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']])) {
|
||||
$uniqueValueValues = $data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']];
|
||||
// in case of site_language we don't have the full form engine options, so fallbacks need to be taken into account
|
||||
$originalUniqueValue = ($uniqueValueValues['table'] ?? $foreignTable) . '_';
|
||||
// @todo In what circumstance would $uniqueValueValues be an array that lacks a 'uid' key? Unclear, but
|
||||
// it breaks the string concatenation. This is a hacky workaround for type safety only.
|
||||
$uVV = ($uniqueValueValues['uid'] ?? $uniqueValueValues);
|
||||
if (is_array($uVV)) {
|
||||
$uVV = implode(',', $uVV);
|
||||
}
|
||||
$originalUniqueValue .= $uVV;
|
||||
}
|
||||
|
||||
// The hashed object id needs a non-numeric prefix, the value is used as ID selector in JavaScript
|
||||
$hashedObjectId = 'hash-' . md5($objectId);
|
||||
$containerAttributes = [
|
||||
'id' => $objectId . '_div',
|
||||
'class' => 'form-irre-object panel panel-default ' . trim(implode(' ', $classes)),
|
||||
'data-object-uid' => $record['uid'] ?? 0,
|
||||
'data-object-id' => $objectId,
|
||||
'data-object-id-hash' => $hashedObjectId,
|
||||
'data-object-parent-group' => $domObjectId . '-' . $foreignTable,
|
||||
'data-field-name' => $appendFormFieldNames,
|
||||
'data-topmost-parent-table' => $data['inlineTopMostParentTableName'],
|
||||
'data-topmost-parent-uid' => $data['inlineTopMostParentUid'],
|
||||
'data-table-unique-original-value' => $originalUniqueValue,
|
||||
'data-placeholder-record' => $data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ? '1' : '0',
|
||||
];
|
||||
|
||||
$isExpanded = $data['isInlineChildExpanded'] ?? false;
|
||||
$ariaControls = htmlspecialchars($objectId . '_fields', ENT_QUOTES | ENT_HTML5);
|
||||
$html = '
|
||||
<div ' . GeneralUtility::implodeAttributes($containerAttributes, true) . '>
|
||||
<div class="panel-heading">
|
||||
<div class="panel-heading-row">
|
||||
' . $this->renderForeignRecordHeader($data, $isExpanded, $ariaControls) . '
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-collapse collapse' . ($isExpanded ? ' show' : '') . '" id="' . $ariaControls . '">' . $html . $combinationHtml . '</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
$resultArray['html'] = $html;
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render inner child
|
||||
*
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function renderChild(array $data)
|
||||
{
|
||||
$domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $data['inlineFirstPid']);
|
||||
$data['tabAndInlineStack'][] = [
|
||||
'inline',
|
||||
$domObjectId . '-' . $data['tableName'] . '-' . $data['databaseRow']['uid'],
|
||||
];
|
||||
// @todo: ugly construct ...
|
||||
$data['inlineData'] = $this->inlineData;
|
||||
$data['renderType'] = 'fullRecordContainer';
|
||||
return $this->nodeFactory->create($data)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render child child
|
||||
*
|
||||
* Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
|
||||
* so two tables are combined (the intermediate table with attributes and the sub-embedded table).
|
||||
* -> This is a direct embedding over two levels!
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function renderCombinationChild(array $data, $appendFormFieldNames)
|
||||
{
|
||||
$childData = $data['combinationChild'];
|
||||
$parentConfig = $data['inlineParentConfig'];
|
||||
|
||||
// If field is set to readOnly, set all fields of the relation to readOnly as well
|
||||
if (isset($parentConfig['readOnly']) && $parentConfig['readOnly']) {
|
||||
foreach ($childData['processedTca']['columns'] as $columnName => $columnConfiguration) {
|
||||
$childData['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Display Warning FlashMessage if it is not suppressed
|
||||
if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
|
||||
$combinationWarningMessage = 'core.core:warning.inline_use_combination';
|
||||
if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
|
||||
$combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
|
||||
}
|
||||
$message = $this->getLanguageService()->sL($combinationWarningMessage);
|
||||
$markup = [];
|
||||
// @TODO: This is not a FlashMessage! The markup must be changed and special CSS
|
||||
// @TODO: should be created, in order to prevent confusion.
|
||||
$markup[] = '<div class="alert alert-warning">';
|
||||
$markup[] = ' <div class="alert-inner">';
|
||||
$markup[] = ' <div class="alert-icon">';
|
||||
$markup[] = ' <span class="icon-emphasized">';
|
||||
$markup[] = ' ' . $this->iconFactory->getIcon('actions-exclamation', IconSize::SMALL)->render();
|
||||
$markup[] = ' </span>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = ' <div class="alert-content">';
|
||||
$markup[] = ' <div class="alert-message">' . htmlspecialchars($message) . '</div>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = '</div>';
|
||||
$resultArray['html'] = implode(LF, $markup);
|
||||
}
|
||||
|
||||
$childArray = $this->renderChild($childData);
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
|
||||
|
||||
// If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
|
||||
if ($childData['command'] === 'new') {
|
||||
$comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
|
||||
$resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
|
||||
}
|
||||
// If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
|
||||
if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
|
||||
$parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
|
||||
$resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
|
||||
}
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc.
|
||||
* Later on the command-icons are inserted here.
|
||||
*
|
||||
* @param array $data Current data
|
||||
* @param bool $isExpanded Whether the record is currently expanded
|
||||
* @param string $ariaControls The ID of the collapse target element
|
||||
* @return string The HTML code of the header
|
||||
*/
|
||||
protected function renderForeignRecordHeader(array $data, bool $isExpanded, string $ariaControls): string
|
||||
{
|
||||
$record = $data['databaseRow'];
|
||||
$recordTitle = $data['recordTitle'];
|
||||
$foreignTable = $data['inlineParentConfig']['foreign_table'];
|
||||
$domObjectId = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $data['inlineFirstPid']);
|
||||
|
||||
if (!empty($recordTitle)) {
|
||||
// The user function may return HTML, therefore we can't escape it
|
||||
if (empty($data['processedTca']['ctrl']['formattedLabel_userFunc'])) {
|
||||
$recordTitle = htmlspecialchars($recordTitle);
|
||||
}
|
||||
} else {
|
||||
$recordTitle = '<em>[' . htmlspecialchars($this->getLanguageService()->sL('core.core:labels.no_title')) . ']</em>';
|
||||
}
|
||||
|
||||
// In case the record title is not generated by a formattedLabel_userFunc, which already
|
||||
// contains custom markup, and we are in debug mode, add the inline record table name.
|
||||
if (empty($data['processedTca']['ctrl']['formattedLabel_userFunc'])
|
||||
&& $this->getBackendUserAuthentication()->shallDisplayDebugInformation()
|
||||
) {
|
||||
$recordTitle .= ' <span class="panel-meta"><code>[' . htmlspecialchars($foreignTable) . ']</code></span>';
|
||||
}
|
||||
|
||||
$objectId = htmlspecialchars($domObjectId . '-' . $foreignTable . '-' . ($record['uid'] ?? 0));
|
||||
return '
|
||||
<button class="panel-button' . ($isExpanded ? '' : ' collapsed') . '" type="button"
|
||||
data-bs-toggle="collapse" data-bs-target="#' . $ariaControls . '"
|
||||
aria-expanded="' . ($isExpanded ? 'true' : 'false') . '" aria-controls="' . $ariaControls . '">
|
||||
<span class="caret"></span>
|
||||
<div class="panel-icon" id="' . $objectId . '_iconcontainer">
|
||||
' . $this->iconFactory->getIconForRecord($foreignTable, $record, IconSize::SMALL, $this->data['tcaSchemata']->get($foreignTable))->setTitle(BackendUtility::getRecordIconAltText($record, $foreignTable, false))->render() . '
|
||||
</div>
|
||||
<div class="panel-title"><span id="' . $objectId . '_label">' . $recordTitle . '</span></div>
|
||||
</button>
|
||||
<div class="panel-actions t3js-formengine-irre-control">
|
||||
' . $this->renderForeignRecordHeaderControl($data) . '
|
||||
</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the control-icons for a record header (create new, sorting, delete, disable/enable).
|
||||
* Most of the parts are copy&paste from TYPO3\CMS\Backend\RecordList\DatabaseRecordList and
|
||||
* modified for the JavaScript calls here
|
||||
*
|
||||
* @param array $data Current data
|
||||
* @return string The HTML code with the control-icons
|
||||
*/
|
||||
protected function renderForeignRecordHeaderControl(array $data)
|
||||
{
|
||||
$rec = $data['databaseRow'];
|
||||
$rec += [
|
||||
'uid' => 0,
|
||||
];
|
||||
$inlineConfig = $data['inlineParentConfig'];
|
||||
$foreignTable = $inlineConfig['foreign_table'];
|
||||
$languageService = $this->getLanguageService();
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
// Initialize:
|
||||
$cells = [
|
||||
'hide' => '',
|
||||
'delete' => '',
|
||||
'info' => '',
|
||||
'new' => '',
|
||||
'sort.up' => '',
|
||||
'sort.down' => '',
|
||||
'dragdrop' => '',
|
||||
'localize' => '',
|
||||
'locked' => '',
|
||||
];
|
||||
$isNewItem = str_starts_with($rec['uid'], 'NEW');
|
||||
$isParentReadOnly = isset($inlineConfig['readOnly']) && $inlineConfig['readOnly'];
|
||||
$isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']);
|
||||
$tableSchema = $this->data['tcaSchemata']->get($foreignTable);
|
||||
$isPagesTable = $foreignTable === 'pages';
|
||||
$enableManualSorting = ($tableSchema->hasCapability(TcaSchemaCapability::SortByField))
|
||||
|| ($inlineConfig['MM'] ?? false)
|
||||
|| (!($data['isOnSymmetricSide'] ?? false) && ($inlineConfig['foreign_sortby'] ?? false))
|
||||
|| (($data['isOnSymmetricSide'] ?? false) && ($inlineConfig['symmetric_sortby'] ?? false));
|
||||
$calcPerms = new Permission($backendUser->calcPerms(BackendUtility::readPageAccess((int)($data['parentPageRow']['uid'] ?? 0), $backendUser->getPagePermsClause(Permission::PAGE_SHOW))));
|
||||
// If the listed table is 'pages' we have to request the permission settings for each page:
|
||||
$localCalcPerms = new Permission(Permission::NOTHING);
|
||||
if ($isPagesTable) {
|
||||
$localCalcPerms = new Permission($backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid'])));
|
||||
}
|
||||
// This expresses the edit permissions for this particular element:
|
||||
$permsEdit = ($isPagesTable && $localCalcPerms->editPagePermissionIsGranted()) || (!$isPagesTable && $calcPerms->editContentPermissionIsGranted());
|
||||
// The event contains all controls and their state (enabled / disabled), which might got modified by listeners
|
||||
$event = $this->eventDispatcher->dispatch(new ModifyInlineElementEnabledControlsEvent($data, $rec));
|
||||
if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$cells['localize'] = $this->iconFactory
|
||||
->getIcon('actions-edit-localize-status-low', IconSize::SMALL)
|
||||
->setTitle($languageService->sL('core.misc:localize.isLocalizable'))
|
||||
->render();
|
||||
}
|
||||
// "Info": (All records)
|
||||
if ($event->isControlEnabled('info')) {
|
||||
if ($isNewItem) {
|
||||
$cells['info'] = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', IconSize::SMALL)->render() . '</span>';
|
||||
} else {
|
||||
$cells['info'] = '
|
||||
<button type="button" class="btn btn-default" data-action="infowindow" data-info-table="' . htmlspecialchars($foreignTable) . '" data-info-uid="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('core.mod_web_list:showInfo')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-document-info', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
}
|
||||
// If the table is NOT a read-only table, then show these links:
|
||||
if (!$isParentReadOnly && !($tableSchema->hasCapability(TcaSchemaCapability::AccessReadOnly)) && !($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false)) {
|
||||
// "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row or if default values can depend on previous record):
|
||||
if ($event->isControlEnabled('new') && ($enableManualSorting || (($tableSchema->getRawConfiguration()['useColumnsForDefaultValues'] ?? false)))) {
|
||||
if ((!$isPagesTable && $calcPerms->editContentPermissionIsGranted()) || ($isPagesTable && $calcPerms->createPagePermissionIsGranted())) {
|
||||
$cells['new'] = '
|
||||
<button type="button" class="btn btn-default t3js-create-new-button" data-record-uid="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('core.mod_web_list:new' . ($isPagesTable ? 'Page' : 'Record'))) . '"' . (!empty($inlineConfig['inline']['hideNewButton']) ? ' hidden' : '') . '>
|
||||
' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page-new' : 'add'), IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
}
|
||||
// "Up/Down" links
|
||||
if ($event->isControlEnabled('sort') && $permsEdit && $enableManualSorting) {
|
||||
// Up
|
||||
$icon = 'actions-move-up';
|
||||
$class = '';
|
||||
if ($inlineConfig['inline']['first'] == $rec['uid']) {
|
||||
$class = ' disabled';
|
||||
$icon = 'empty-empty';
|
||||
}
|
||||
$cells['sort.up'] = '
|
||||
<button type="button" class="btn btn-default' . $class . '" data-action="sort" data-direction="up" title="' . htmlspecialchars($languageService->sL('core.mod_web_list:moveUp')) . '">
|
||||
' . $this->iconFactory->getIcon($icon, IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
// Down
|
||||
$icon = 'actions-move-down';
|
||||
$class = '';
|
||||
if ($inlineConfig['inline']['last'] == $rec['uid']) {
|
||||
$class = ' disabled';
|
||||
$icon = 'empty-empty';
|
||||
}
|
||||
|
||||
$cells['sort.down'] = '
|
||||
<button type="button" class="btn btn-default' . $class . '" data-action="sort" data-direction="down" title="' . htmlspecialchars($languageService->sL('core.mod_web_list:moveDown')) . '">
|
||||
' . $this->iconFactory->getIcon($icon, IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
// "Delete" link:
|
||||
if ($event->isControlEnabled('delete')
|
||||
&& (
|
||||
($isPagesTable && $localCalcPerms->deletePagePermissionIsGranted())
|
||||
|| (!$isPagesTable && $calcPerms->editContentPermissionIsGranted())
|
||||
)
|
||||
) {
|
||||
$title = htmlspecialchars($languageService->sL('core.mod_web_list:delete'));
|
||||
$icon = $this->iconFactory->getIcon('actions-edit-delete', IconSize::SMALL)->render();
|
||||
|
||||
$recordInfo = $data['databaseRow']['uid_local'][0]['title'] ?? $data['recordTitle'] ?? '';
|
||||
if ($this->getBackendUserAuthentication()->shallDisplayDebugInformation()) {
|
||||
$recordInfo .= ' [' . $data['tableName'] . ':' . $data['vanillaUid'] . ']';
|
||||
}
|
||||
|
||||
$cells['delete'] = '
|
||||
<button type="button" class="btn btn-default t3js-editform-delete-inline-record" data-record-info="' . htmlspecialchars(trim($recordInfo)) . '" title="' . $title . '">
|
||||
' . $icon . '
|
||||
</button>';
|
||||
}
|
||||
|
||||
// "Hide/Unhide" links:
|
||||
$hiddenField = $tableSchema->hasCapability(TcaSchemaCapability::RestrictionDisabledField) ? $tableSchema->getCapability(TcaSchemaCapability::RestrictionDisabledField)->getFieldName() : '';
|
||||
if ($event->isControlEnabled('hide')
|
||||
&& $permsEdit
|
||||
&& $hiddenField
|
||||
&& ($tableSchema->hasField($hiddenField) ?? false)
|
||||
&& (!($tableSchema->getField($hiddenField)->getConfiguration()['exclude'] ?? false) || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))
|
||||
) {
|
||||
if ($rec[$hiddenField]) {
|
||||
$title = htmlspecialchars($languageService->sL('core.mod_web_list:unHide' . ($isPagesTable ? 'Page' : '')));
|
||||
$cells['hide'] = '
|
||||
<button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . $title . '">
|
||||
' . $this->iconFactory->getIcon('actions-edit-unhide', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
} else {
|
||||
$title = htmlspecialchars($languageService->sL('core.mod_web_list:hide' . ($isPagesTable ? 'Page' : '')));
|
||||
$cells['hide'] = '
|
||||
<button type="button" class="btn btn-default t3js-toggle-visibility-button" data-hidden-field="' . htmlspecialchars($hiddenField) . '" title="' . $title . '">
|
||||
' . $this->iconFactory->getIcon('actions-edit-hide', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
}
|
||||
// Drag&Drop Sorting: Sortable handle
|
||||
if ($event->isControlEnabled('dragdrop') && $permsEdit && $enableManualSorting && ($inlineConfig['appearance']['useSortable'] ?? false)) {
|
||||
$cells['dragdrop'] = '
|
||||
<span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('core.core:labels.move')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-move-move', IconSize::SMALL)->render() . '
|
||||
</span>';
|
||||
}
|
||||
} elseif (($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false) && $isParentExisting) {
|
||||
if ($event->isControlEnabled('localize') && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$cells['localize'] = '
|
||||
<button type="button" class="btn btn-default t3js-synchronizelocalize-button" data-type="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('core.misc:localize')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-document-localize', IconSize::SMALL)->render() . '
|
||||
</button>';
|
||||
}
|
||||
}
|
||||
// If the record is edit-locked by another user, we will show a little warning sign:
|
||||
if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
|
||||
$cells['locked'] = '
|
||||
<button type="button" class="btn btn-default" title="' . htmlspecialchars($lockInfo['msg']) . '">
|
||||
' . $this->iconFactory->getIcon('status-user-backend', IconSize::SMALL, 'overlay-edit')->render() . '
|
||||
</button>';
|
||||
}
|
||||
|
||||
// Get modified controls. This means their markup was modified, new controls were added or controls got removed.
|
||||
$cells = $this->eventDispatcher->dispatch(new ModifyInlineElementControlsEvent($cells, $data, $rec))->getControls();
|
||||
|
||||
$out = '';
|
||||
if (!empty($cells['hide']) || !empty($cells['delete'])) {
|
||||
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['hide'] . $cells['delete'] . '</div>';
|
||||
unset($cells['hide'], $cells['delete']);
|
||||
}
|
||||
if (!empty($cells['info']) || !empty($cells['new']) || !empty($cells['sort.up']) || !empty($cells['sort.down']) || !empty($cells['dragdrop'])) {
|
||||
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['info'] . $cells['new'] . $cells['sort.up'] . $cells['sort.down'] . $cells['dragdrop'] . '</div>';
|
||||
unset($cells['info'], $cells['new'], $cells['sort.up'], $cells['sort.down'], $cells['dragdrop']);
|
||||
}
|
||||
if (!empty($cells['localize'])) {
|
||||
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['localize'] . '</div>';
|
||||
unset($cells['localize']);
|
||||
}
|
||||
if (!empty($cells)) {
|
||||
$cellContent = trim(implode('', $cells));
|
||||
$out .= $cellContent !== '' ? ' <div class="btn-group btn-group-sm" role="group">' . $cellContent . '</div>' : '';
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Render a given list of field of a TCA table.
|
||||
*
|
||||
* This is an entry container called from FormEngine to handle a
|
||||
* list of specific fields. Access rights are checked here and globalOption array
|
||||
* is prepared for further processing of single fields by PaletteAndSingleContainer.
|
||||
*
|
||||
* Using "hiddenFieldListToRender" it's also possible to render additional fields as
|
||||
* hidden fields, which is e.g. used for the "generatorFields" of TCA type "slug".
|
||||
*/
|
||||
class ListOfFieldsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$options = $this->data;
|
||||
$options['fieldsArray'] = $this->sanitizeFieldList($this->data['fieldListToRender']);
|
||||
|
||||
if ($this->data['hiddenFieldListToRender'] ?? false) {
|
||||
$hiddenFieldList = array_diff(
|
||||
$this->sanitizeFieldList($this->data['hiddenFieldListToRender']),
|
||||
$options['fieldsArray']
|
||||
);
|
||||
if ($hiddenFieldList !== []) {
|
||||
$hiddenFieldList = implode(',', $hiddenFieldList);
|
||||
$hiddenPaletteName = 'hiddenFieldsPalette' . md5($hiddenFieldList);
|
||||
$options['processedTca']['palettes'][$hiddenPaletteName] = [
|
||||
'isHiddenPalette' => true,
|
||||
'showitem' => $hiddenFieldList,
|
||||
];
|
||||
$options['fieldsArray'][] = '--palette--;;' . $hiddenPaletteName;
|
||||
}
|
||||
}
|
||||
|
||||
$options['renderType'] = 'paletteAndSingleContainer';
|
||||
return $this->nodeFactory->create($options)->render();
|
||||
}
|
||||
|
||||
protected function sanitizeFieldList(string $fieldList): array
|
||||
{
|
||||
$fields = array_unique(GeneralUtility::trimExplode(',', $fieldList, true));
|
||||
$fieldsByShowitem = $this->data['processedTca']['types'][$this->data['recordTypeValue']]['showitem'];
|
||||
$fieldsByShowitem = GeneralUtility::trimExplode(',', $fieldsByShowitem, true);
|
||||
|
||||
$allowedFields = [];
|
||||
foreach ($fields as $fieldName) {
|
||||
foreach ($fieldsByShowitem as $fieldByShowitem) {
|
||||
$fieldByShowitemArray = $this->explodeSingleFieldShowItemConfiguration($fieldByShowitem);
|
||||
if ($fieldByShowitemArray['fieldName'] === $fieldName) {
|
||||
$allowedFields[] = implode(';', $fieldByShowitemArray);
|
||||
break;
|
||||
}
|
||||
if ($fieldByShowitemArray['fieldName'] === '--palette--'
|
||||
&& isset($this->data['processedTca']['palettes'][$fieldByShowitemArray['paletteName']]['showitem'])
|
||||
&& is_string($this->data['processedTca']['palettes'][$fieldByShowitemArray['paletteName']]['showitem'])
|
||||
) {
|
||||
$paletteName = $fieldByShowitemArray['paletteName'];
|
||||
$paletteFields = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true);
|
||||
foreach ($paletteFields as $paletteField) {
|
||||
$paletteFieldArray = $this->explodeSingleFieldShowItemConfiguration($paletteField);
|
||||
if ($paletteFieldArray['fieldName'] === $fieldName) {
|
||||
$allowedFields[] = implode(';', $paletteFieldArray);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $allowedFields;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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\Container;
|
||||
|
||||
/**
|
||||
* Handle a record that has no tabs.
|
||||
*
|
||||
* This container is called by FullRecordContainer.
|
||||
*/
|
||||
class NoTabsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$options = $this->data;
|
||||
$options['renderType'] = 'paletteAndSingleContainer';
|
||||
return $this->nodeFactory->create($options)->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle palettes and single fields.
|
||||
*
|
||||
* This container is called by TabsContainer, NoTabsContainer and ListOfFieldsContainer.
|
||||
*
|
||||
* This container mostly operates on TCA showItem of a specific type - the value is
|
||||
* coming in from upper containers as "fieldArray". It handles palettes with all its
|
||||
* different options and prepares rendering of single fields for the SingleFieldContainer.
|
||||
*/
|
||||
class PaletteAndSingleContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Final result array accumulating results from children and final HTML
|
||||
*/
|
||||
protected array $resultArray = [];
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
/*
|
||||
* The first code block creates a target structure array to later create the final
|
||||
* HTML string. The single fields and sub containers are rendered here already and
|
||||
* other parts of the return array from children except html are accumulated in
|
||||
* $this->resultArray
|
||||
*
|
||||
$targetStructure = [
|
||||
0 => [
|
||||
'type' => 'palette',
|
||||
'fieldName' => 'palette1',
|
||||
'paletteLegend' => 'palette1',
|
||||
'paletteDescription' => 'palette1Description',
|
||||
'elements' => [
|
||||
0 => [
|
||||
'type' => 'single',
|
||||
'fieldName' => 'paletteName',
|
||||
'fieldHtml' => 'element1',
|
||||
),
|
||||
1 => [
|
||||
'type' => 'linebreak',
|
||||
),
|
||||
2 => [
|
||||
'type' => 'single',
|
||||
'fieldName' => 'paletteName',
|
||||
'fieldHtml' => 'element2',
|
||||
],
|
||||
],
|
||||
],
|
||||
1 => [
|
||||
'type' => 'single',
|
||||
'fieldName' => 'element3',
|
||||
'fieldHtml' => 'element3',
|
||||
],
|
||||
2 => [
|
||||
'type' => 'palette',
|
||||
'fieldName' => 'palette2',
|
||||
'paletteLegend' => '', // Palette label is optional
|
||||
'paletteDescription' => '', // Palette description is optional
|
||||
'elements' => [
|
||||
0 => [
|
||||
'type' => 'single',
|
||||
'fieldName' => 'element4',
|
||||
'fieldHtml' => 'element4',
|
||||
],
|
||||
1 => [
|
||||
'type' => 'linebreak',
|
||||
],
|
||||
2 => [
|
||||
'type' => 'single',
|
||||
'fieldName' => 'element5',
|
||||
'fieldHtml' => 'element5',
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
*/
|
||||
|
||||
// Create an intermediate structure of rendered sub elements and elements nested in palettes
|
||||
$targetStructure = [];
|
||||
$mainStructureCounter = -1;
|
||||
$fieldsArray = $this->data['fieldsArray'];
|
||||
$this->resultArray = $this->initializeResultArray();
|
||||
foreach ($fieldsArray as $fieldString) {
|
||||
$fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($fieldString);
|
||||
$fieldName = $fieldConfiguration['fieldName'];
|
||||
if ($fieldName === '--palette--') {
|
||||
$paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName'] ?? '');
|
||||
if (!empty($paletteElementArray)) {
|
||||
$mainStructureCounter++;
|
||||
// If there is no label in ['types']['aType']['showitem'] for this palette: "--palette--;;aPalette",
|
||||
// then use ['palettes']['aPalette']['label'] if given.
|
||||
$paletteLegend = $fieldConfiguration['fieldLabel'];
|
||||
if ($paletteLegend === null && !empty($this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['label'])) {
|
||||
$paletteLegend = $this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['label'];
|
||||
}
|
||||
// Get description of palette.
|
||||
$paletteDescription = $this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['description'] ?? '';
|
||||
$targetStructure[$mainStructureCounter] = [
|
||||
'type' => 'palette',
|
||||
'fieldName' => $fieldConfiguration['paletteName'],
|
||||
'paletteLegend' => $languageService->sL($paletteLegend),
|
||||
'paletteDescription' => $languageService->sL($paletteDescription),
|
||||
'elements' => $paletteElementArray,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
if (!is_array($this->data['processedTca']['columns'][$fieldName] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
$options = $this->data;
|
||||
$options['fieldName'] = $fieldName;
|
||||
$options['renderType'] = 'singleFieldContainer';
|
||||
$childResultArray = $this->nodeFactory->create($options)->render();
|
||||
if (!empty($childResultArray['html'])) {
|
||||
$mainStructureCounter++;
|
||||
$targetStructure[$mainStructureCounter] = [
|
||||
'type' => 'single',
|
||||
'fieldName' => $fieldConfiguration['fieldName'],
|
||||
'fieldHtml' => $childResultArray['html'],
|
||||
];
|
||||
}
|
||||
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Compile final content
|
||||
$content = [];
|
||||
foreach ($targetStructure as $element) {
|
||||
if ($element['type'] === 'palette') {
|
||||
$paletteName = $element['fieldName'];
|
||||
$isHiddenPalette = !empty($this->data['processedTca']['palettes'][$paletteName]['isHiddenPalette']);
|
||||
$html = [];
|
||||
$html[] = '<fieldset class="form-section' . ($isHiddenPalette ? ' hide' : '') . '">';
|
||||
if (!empty($element['paletteLegend'])) {
|
||||
$html[] = '<h3 class="form-section-headline">' . htmlspecialchars($element['paletteLegend']) . '</h3>';
|
||||
}
|
||||
if (!empty($element['paletteDescription'])) {
|
||||
$html[] = '<p class="form-section-description">' . nl2br(htmlspecialchars($element['paletteDescription'])) . '</p>';
|
||||
}
|
||||
$html[] = $this->renderInnerPaletteContent($element);
|
||||
$html[] = '</fieldset>';
|
||||
$content[] = implode(LF, $html);
|
||||
} else {
|
||||
$html = [];
|
||||
$html[] = '<fieldset class="form-section">';
|
||||
$html[] = '<div class="form-group t3js-formengine-validation-marker t3js-formengine-palette-field">';
|
||||
$html[] = $element['fieldHtml'];
|
||||
$html[] = '</div>';
|
||||
$html[] = '</fieldset>';
|
||||
$content[] = implode(LF, $html);
|
||||
}
|
||||
}
|
||||
|
||||
$finalResultArray = $this->resultArray;
|
||||
$finalResultArray['html'] = implode(LF, $content);
|
||||
return $finalResultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single fields of a given palette
|
||||
*
|
||||
* @param string $paletteName The palette to render
|
||||
*/
|
||||
protected function createPaletteContentArray(string $paletteName): array
|
||||
{
|
||||
// palette needs a palette name reference, otherwise it does not make sense to try rendering of it
|
||||
if (empty($paletteName) || empty($this->data['processedTca']['palettes'][$paletteName]['showitem'])) {
|
||||
return [];
|
||||
}
|
||||
$resultStructure = [];
|
||||
$foundRealElement = false; // Set to true if not only line breaks were rendered
|
||||
$fieldsArray = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true);
|
||||
foreach ($fieldsArray as $fieldString) {
|
||||
$fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
|
||||
$fieldName = $fieldArray['fieldName'];
|
||||
if ($fieldName === '--linebreak--') {
|
||||
$resultStructure[] = [
|
||||
'type' => 'linebreak',
|
||||
];
|
||||
} else {
|
||||
if (!is_array($this->data['processedTca']['columns'][$fieldName] ?? null)) {
|
||||
continue;
|
||||
}
|
||||
$options = $this->data;
|
||||
$options['fieldName'] = $fieldName;
|
||||
$options['renderType'] = 'singleFieldContainer';
|
||||
$singleFieldContentArray = $this->nodeFactory->create($options)->render();
|
||||
if (!empty($singleFieldContentArray['html'])) {
|
||||
$foundRealElement = true;
|
||||
$resultStructure[] = [
|
||||
'type' => 'single',
|
||||
'fieldName' => $fieldName,
|
||||
'fieldHtml' => $singleFieldContentArray['html'],
|
||||
];
|
||||
}
|
||||
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray, false);
|
||||
}
|
||||
}
|
||||
if ($foundRealElement) {
|
||||
return $resultStructure;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders inner content of single elements of a palette and wrap it as needed
|
||||
*
|
||||
* @param array $elementArray Array of elements
|
||||
* @return string Wrapped content
|
||||
*/
|
||||
protected function renderInnerPaletteContent(array $elementArray): string
|
||||
{
|
||||
$result = [];
|
||||
$currentGroup = [];
|
||||
|
||||
foreach ($elementArray['elements'] as $element) {
|
||||
if ($element['type'] === 'linebreak') {
|
||||
// Render current group before linebreak
|
||||
if (!empty($currentGroup)) {
|
||||
$result[] = $this->renderFieldGroup($currentGroup);
|
||||
$currentGroup = [];
|
||||
}
|
||||
} else {
|
||||
$currentGroup[] = $element;
|
||||
}
|
||||
}
|
||||
|
||||
// Render remaining group
|
||||
if (!empty($currentGroup)) {
|
||||
$result[] = $this->renderFieldGroup($currentGroup);
|
||||
}
|
||||
|
||||
return implode(LF, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a group of fields within a form-grid container
|
||||
*
|
||||
* @param array $fields Array of field elements
|
||||
* @return string Rendered HTML
|
||||
*/
|
||||
protected function renderFieldGroup(array $fields): string
|
||||
{
|
||||
$numberOfItems = count($fields);
|
||||
$result = [];
|
||||
|
||||
if ($numberOfItems > 1) {
|
||||
$result[] = '<div class="form-grid" style="--typo3-form-grid-columns: ' . $numberOfItems . '">';
|
||||
}
|
||||
|
||||
foreach ($fields as $element) {
|
||||
$result[] = '<div class="form-group t3js-formengine-validation-marker t3js-formengine-palette-field">';
|
||||
$result[] = $element['fieldHtml'];
|
||||
$result[] = '</div>';
|
||||
}
|
||||
|
||||
if ($numberOfItems > 1) {
|
||||
$result[] = '</div>';
|
||||
}
|
||||
|
||||
return implode(LF, $result);
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Behavior\ReloadOnFieldChange;
|
||||
use TYPO3\CMS\Backend\Form\Behavior\UpdateValueOnFieldChange;
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
|
||||
use TYPO3\CMS\Core\Authentication\JsConfirmation;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Container around a "single field".
|
||||
*
|
||||
* This container is the last one in the chain before processing is handed over to single element classes.
|
||||
* If a single field is of type flex or inline, it however creates FlexFormEntryContainer or InlineControlContainer.
|
||||
*
|
||||
* The container does various checks and processing for a given single fields.
|
||||
*/
|
||||
class SingleFieldContainer extends AbstractContainer
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InlineStackProcessor $inlineStackProcessor
|
||||
) {}
|
||||
|
||||
public function render(): array
|
||||
{
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$fieldName = $this->data['fieldName'];
|
||||
|
||||
$parameterArray = [];
|
||||
$parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName];
|
||||
|
||||
$isOverlay = false;
|
||||
|
||||
// This field decides whether the current record is an overlay (as opposed to being a standalone record)
|
||||
// Based on this decision we need to trigger field exclusion or special rendering (like readOnly)
|
||||
if (isset($this->data['processedTca']['ctrl']['transOrigPointerField'])
|
||||
&& is_array($this->data['processedTca']['columns'][$this->data['processedTca']['ctrl']['transOrigPointerField']] ?? null)
|
||||
) {
|
||||
$parentValue = $row[$this->data['processedTca']['ctrl']['transOrigPointerField']];
|
||||
if (MathUtility::canBeInterpretedAsInteger($parentValue)) {
|
||||
$isOverlay = (bool)$parentValue;
|
||||
} elseif (is_array($parentValue)) {
|
||||
// This case may apply if the value has been converted to an array by the select or group data provider
|
||||
$isOverlay = !empty($parentValue) ? (bool)$parentValue[0] : false;
|
||||
} else {
|
||||
throw new \InvalidArgumentException(
|
||||
'The given value "' . $parentValue . '" for the original language field ' . $this->data['processedTca']['ctrl']['transOrigPointerField']
|
||||
. ' of table ' . $table . ' is invalid.',
|
||||
1470742770
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A couple of early returns in case the field should not be rendered
|
||||
$fieldIsExcluded = $parameterArray['fieldConf']['exclude'] ?? false;
|
||||
$fieldNotExcludable = $backendUser->check('non_exclude_fields', $table . ':' . $fieldName);
|
||||
$fieldExcludedFromTranslatedRecords = empty($parameterArray['fieldConf']['l10n_display']) && ($parameterArray['fieldConf']['l10n_mode'] ?? '') === 'exclude';
|
||||
// Return if BE-user has no access rights to this field, @todo: another user access rights check!
|
||||
if (($fieldIsExcluded && !$fieldNotExcludable) || ($isOverlay && $fieldExcludedFromTranslatedRecords) || $this->inlineFieldShouldBeSkipped()) {
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$tsConfig = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'] ?? [];
|
||||
$parameterArray['fieldTSConfig'] = is_array($tsConfig) ? $tsConfig : [];
|
||||
|
||||
if ($parameterArray['fieldTSConfig']['disabled'] ?? false) {
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
// Override fieldConf by fieldTSconfig:
|
||||
$parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
|
||||
$parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
|
||||
$newElementBaseName = isset($this->data['elementBaseName']) ? $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']' : '';
|
||||
|
||||
// The value to show in the form field.
|
||||
$parameterArray['itemFormElValue'] = $row[$fieldName];
|
||||
// Set field to read-only if configured for translated records to show default language content as readonly
|
||||
// Note: In such case, the database value of this field was already overridden by DatabaseRowDefaultAsReadonly.
|
||||
if (($parameterArray['fieldConf']['l10n_display'] ?? false)
|
||||
&& GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
|
||||
&& $isOverlay
|
||||
) {
|
||||
$parameterArray['fieldConf']['config']['readOnly'] = true;
|
||||
}
|
||||
|
||||
$processedTcaType = $this->data['processedTca']['ctrl']['type'] ?? '';
|
||||
$typeField = !str_contains($processedTcaType, ':')
|
||||
? $processedTcaType
|
||||
: substr($processedTcaType, 0, (int)strpos($processedTcaType, ':'));
|
||||
|
||||
// JavaScript code for event handlers:
|
||||
$parameterArray['fieldChangeFunc'] = [];
|
||||
$parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = new UpdateValueOnFieldChange(
|
||||
$table,
|
||||
(string)$row['uid'],
|
||||
$fieldName,
|
||||
$parameterArray['itemFormElName']
|
||||
);
|
||||
|
||||
$requestFormEngineUpdate
|
||||
= (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField)
|
||||
|| (isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload');
|
||||
if ($requestFormEngineUpdate) {
|
||||
$askForUpdate = $backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE);
|
||||
$parameterArray['fieldChangeFunc']['record_type_changed'] = new ReloadOnFieldChange($askForUpdate);
|
||||
}
|
||||
|
||||
// Based on the type of the item, call a render function on a child element
|
||||
$options = $this->data;
|
||||
$options['parameterArray'] = $parameterArray;
|
||||
$options['elementBaseName'] = $newElementBaseName;
|
||||
if (!empty($parameterArray['fieldConf']['config']['renderType'])) {
|
||||
$options['renderType'] = $parameterArray['fieldConf']['config']['renderType'];
|
||||
} else {
|
||||
// Fallback to type if no renderType is given
|
||||
$options['renderType'] = $parameterArray['fieldConf']['config']['type'];
|
||||
}
|
||||
|
||||
return $this->nodeFactory->create($options)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering of inline fields should be skipped under certain circumstances
|
||||
*/
|
||||
protected function inlineFieldShouldBeSkipped(): bool
|
||||
{
|
||||
$table = $this->data['tableName'];
|
||||
$fieldName = $this->data['fieldName'];
|
||||
$fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config'];
|
||||
$fieldConfig += [
|
||||
'MM' => '',
|
||||
'foreign_table' => '',
|
||||
'foreign_selector' => '',
|
||||
'foreign_field' => '',
|
||||
];
|
||||
if (($this->data['inlineStructure']['stable'] ?? []) !== []) {
|
||||
$searchArray = [
|
||||
'%OR' => [
|
||||
'config' => [
|
||||
0 => [
|
||||
'%AND' => [
|
||||
'foreign_table' => $table,
|
||||
'%OR' => [
|
||||
'%AND' => [
|
||||
'appearance' => ['useCombination' => true],
|
||||
'foreign_selector' => $fieldName,
|
||||
],
|
||||
'MM' => $fieldConfig['MM'],
|
||||
],
|
||||
],
|
||||
],
|
||||
1 => [
|
||||
'%AND' => [
|
||||
'foreign_table' => $fieldConfig['foreign_table'],
|
||||
'foreign_selector' => $fieldConfig['foreign_field'],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
// If we have symmetric fields, check on which side we are and hide fields, that are set automatically:
|
||||
if ($this->data['isOnSymmetricSide']) {
|
||||
$searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName;
|
||||
$searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName;
|
||||
} else {
|
||||
$searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName;
|
||||
$searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName;
|
||||
}
|
||||
// Parent record from structure stack
|
||||
$parent = $this->inlineStackProcessor->getStructureLevelFromStructure($this->data['inlineStructure'], -1) ?? [];
|
||||
return $this->arrayCompareComplex($parent, $searchArray);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles complex comparison requests on an array.
|
||||
* A request could look like the following:
|
||||
*
|
||||
* $searchArray = array(
|
||||
* '%AND' => array(
|
||||
* 'key1' => 'value1',
|
||||
* 'key2' => 'value2',
|
||||
* '%OR' => array(
|
||||
* 'subarray' => array(
|
||||
* 'subkey' => 'subvalue'
|
||||
* ),
|
||||
* 'key3' => 'value3',
|
||||
* 'key4' => 'value4'
|
||||
* )
|
||||
* )
|
||||
* );
|
||||
*
|
||||
* It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent
|
||||
* overwriting the sub-array. It could be necessary, if you use complex comparisons.
|
||||
*
|
||||
* The example above means, key1 *AND* key2 (and their values) have to match with
|
||||
* the $subjectArray and additional one *OR* key3 or key4 have to meet the same
|
||||
* condition.
|
||||
* It is also possible to compare parts of a sub-array (e.g. "subarray"), so this
|
||||
* function recurses down one level in that sub-array.
|
||||
*
|
||||
* @param array $subjectArray The array to search in
|
||||
* @param array $searchArray The array with keys and values to search for
|
||||
* @param string $type Use '%AND' or '%OR' for comparison
|
||||
* @return bool The result of the comparison
|
||||
*/
|
||||
protected function arrayCompareComplex(array $subjectArray, array $searchArray, string $type = ''): bool
|
||||
{
|
||||
$localMatches = 0;
|
||||
$localEntries = 0;
|
||||
if ($searchArray !== []) {
|
||||
// If no type was passed, try to determine
|
||||
if (!$type) {
|
||||
reset($searchArray);
|
||||
$type = (string)key($searchArray);
|
||||
$searchArray = current($searchArray);
|
||||
}
|
||||
// We use '%AND' and '%OR' in uppercase
|
||||
$type = strtoupper($type);
|
||||
// Split regular elements from sub elements
|
||||
foreach ($searchArray as $key => $value) {
|
||||
$localEntries++;
|
||||
// Process a sub-group of OR-conditions
|
||||
if ($key === '%OR') {
|
||||
$localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0;
|
||||
} elseif ($key === '%AND') {
|
||||
$localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0;
|
||||
} elseif (is_array($value) && $this->isAssociativeArray($searchArray)) {
|
||||
$localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0;
|
||||
} elseif (is_array($value)) {
|
||||
$localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0;
|
||||
} else {
|
||||
if (isset($subjectArray[$key]) && isset($value)) {
|
||||
// Boolean match:
|
||||
if (is_bool($value)) {
|
||||
$localMatches += !($subjectArray[$key] xor $value) ? 1 : 0;
|
||||
} elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) {
|
||||
$localMatches += $subjectArray[$key] == $value ? 1 : 0;
|
||||
} else {
|
||||
$localMatches += $subjectArray[$key] === $value ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If one or more matches are required ('OR'), return TRUE after the first successful match
|
||||
if ($type === '%OR' && $localMatches > 0) {
|
||||
return true;
|
||||
}
|
||||
// If all matches are required ('AND') and we have no result after the first run, return FALSE
|
||||
if ($type === '%AND' && $localMatches == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return the result for '%AND' (if nothing was checked, TRUE is returned)
|
||||
return $localEntries === $localMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an object is an associative array.
|
||||
*
|
||||
* @param mixed $object The object to be checked
|
||||
* @return bool Returns TRUE, if the object is an associative array
|
||||
*/
|
||||
protected function isAssociativeArray($object)
|
||||
{
|
||||
return is_array($object) && !empty($object) && array_keys($object) !== range(0, count($object) - 1);
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Site\SiteLanguagePresets;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Site languages entry container
|
||||
*
|
||||
* @internal This container is only used in the site configuration module and is not public API
|
||||
*/
|
||||
class SiteLanguageContainer extends AbstractContainer
|
||||
{
|
||||
private const string FOREIGN_TABLE = 'site_language';
|
||||
private const string FOREIGN_FIELD = 'languageId';
|
||||
|
||||
protected array $inlineData;
|
||||
|
||||
public function __construct(
|
||||
private readonly InlineStackProcessor $inlineStackProcessor,
|
||||
private readonly SiteLanguagePresets $siteLanguagePresets,
|
||||
private readonly HashService $hashService,
|
||||
) {}
|
||||
|
||||
public function render(): array
|
||||
{
|
||||
$this->inlineData = $this->data['inlineData'];
|
||||
|
||||
$inlineStructure = $this->data['inlineStructure'];
|
||||
|
||||
$row = $this->data['databaseRow'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$config = $parameterArray['fieldConf']['config'];
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Add the current inline job to the structure stack
|
||||
$inlineStructure['stable'][] = [
|
||||
'table' => $this->data['tableName'],
|
||||
'uid' => $row['uid'],
|
||||
'field' => $this->data['fieldName'],
|
||||
'config' => $config,
|
||||
];
|
||||
|
||||
// Hand over original returnUrl to SiteInlineAjaxController. Needed if opening for instance a
|
||||
// nested element in a new view to then go back to the original returnUrl and not the url of
|
||||
// the site inline ajax controller.
|
||||
$config['originalReturnUrl'] = $this->data['returnUrl'];
|
||||
|
||||
// e.g. data[site][1][languages]
|
||||
$nameForm = $this->inlineStackProcessor->getFormPrefixFromStructure($inlineStructure);
|
||||
// e.g. data-0-site-1-languages
|
||||
$nameObject = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($inlineStructure, $this->data['inlineFirstPid']);
|
||||
// e.g. array('table' => 'site', 'uid' => '1', 'field' => 'languages', 'config' => array())
|
||||
$top = $this->inlineStackProcessor->getStructureLevelFromStructure($inlineStructure, 0);
|
||||
|
||||
$this->inlineData['config'][$nameObject] = [
|
||||
'table' => self::FOREIGN_TABLE,
|
||||
];
|
||||
|
||||
$configJson = (string)json_encode($config);
|
||||
$this->inlineData['config'][$nameObject . '-' . self::FOREIGN_TABLE] = [
|
||||
'min' => $config['minitems'],
|
||||
'max' => $config['maxitems'],
|
||||
'sortable' => false,
|
||||
'top' => [
|
||||
'table' => $top['table'],
|
||||
'uid' => $top['uid'],
|
||||
],
|
||||
'context' => [
|
||||
'config' => $configJson,
|
||||
'hmac' => $this->hashService->hmac($configJson, 'InlineContext'),
|
||||
],
|
||||
];
|
||||
$this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
|
||||
|
||||
$uniqueIds = [];
|
||||
foreach ($parameterArray['fieldConf']['children'] as $children) {
|
||||
$value = (int)($children['databaseRow'][self::FOREIGN_FIELD]['0'] ?? 0);
|
||||
if (isset($children['databaseRow']['uid'])) {
|
||||
$uniqueIds[$children['databaseRow']['uid']] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$uniquePossibleRecords = $config['uniquePossibleRecords'] ?? [];
|
||||
$possibleRecordsUidToTitle = [];
|
||||
foreach ($uniquePossibleRecords as $possibleRecord) {
|
||||
$possibleRecordsUidToTitle[$possibleRecord['value']] = $possibleRecord['label'];
|
||||
}
|
||||
$this->inlineData['unique'][$nameObject . '-' . self::FOREIGN_TABLE] = [
|
||||
// Usually "max" would be the number of possible records. However, since
|
||||
// we also allow new languages to be created, we just use the maxitems value.
|
||||
'max' => $config['maxitems'],
|
||||
// "used" must be a string array
|
||||
'used' => array_map(strval(...), $uniqueIds),
|
||||
'table' => self::FOREIGN_TABLE,
|
||||
'elTable' => self::FOREIGN_TABLE,
|
||||
'field' => self::FOREIGN_FIELD,
|
||||
'possible' => $possibleRecordsUidToTitle,
|
||||
];
|
||||
|
||||
$resultArray['inlineData'] = $this->inlineData;
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
$selectorOptions = $childRecordUids = $childHtml = [];
|
||||
|
||||
foreach ($config['uniquePossibleRecords'] ?? [] as $record) {
|
||||
// Do not add the PHP_INT_MAX placeholder or already configured languages
|
||||
if ($record['value'] !== PHP_INT_MAX && !in_array($record['value'], $uniqueIds, true)) {
|
||||
$selectorOptions[] = ['value' => (string)$record['value'], 'label' => (string)$record['label']];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->data['parameterArray']['fieldConf']['children'] as $children) {
|
||||
$children['inlineParentUid'] = $row['uid'];
|
||||
$children['inlineFirstPid'] = $this->data['inlineFirstPid'];
|
||||
$children['inlineParentConfig'] = $config;
|
||||
$children['inlineData'] = $this->inlineData;
|
||||
$children['inlineStructure'] = $inlineStructure;
|
||||
$children['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
|
||||
$children['renderType'] = 'inlineRecordContainer';
|
||||
$childResult = $this->nodeFactory->create($children)->render();
|
||||
$childHtml[] = $childResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
|
||||
if (isset($children['databaseRow']['uid'])) {
|
||||
$childRecordUids[] = $children['databaseRow']['uid'];
|
||||
}
|
||||
}
|
||||
|
||||
$view = $this->backendViewFactory->create($this->data['request']);
|
||||
$view->assignMultiple([
|
||||
'nameObject' => $nameObject,
|
||||
'nameForm' => $nameForm,
|
||||
'webComponentAttributes' => GeneralUtility::implodeAttributes([
|
||||
'id' => $nameObject,
|
||||
'data-type' => 'language',
|
||||
'data-object-group' => $nameObject . '-' . self::FOREIGN_TABLE,
|
||||
'data-form-field' => $nameForm,
|
||||
'data-expand-single' => (bool)($config['appearance']['expandSingle'] ?? false) ? 'true' : 'false',
|
||||
'data-sortable' => 'false',
|
||||
'data-min' => (int)($config['minitems'] ?? 0),
|
||||
'data-max' => (int)($config['maxitems'] ?? 0),
|
||||
], true),
|
||||
'fieldInformation' => $fieldInformationResult['html'],
|
||||
'selectorConfiguration' => [
|
||||
'identifier' => $nameObject . '-' . self::FOREIGN_TABLE . '_selector',
|
||||
'options' => $selectorOptions,
|
||||
],
|
||||
'inlineRecords' => [
|
||||
'identifier' => $nameObject . '_records',
|
||||
'title' => trim($parameterArray['fieldConf']['label'] ?? ''),
|
||||
'records' => implode(PHP_EOL, $childHtml),
|
||||
],
|
||||
'childRecordUids' => implode(',', $childRecordUids),
|
||||
'validationRules' => $this->getValidationDataAsJsonString([
|
||||
'type' => 'inline',
|
||||
'minitems' => $config['minitems'] ?? null,
|
||||
'maxitems' => $config['maxitems'] ?? null,
|
||||
]),
|
||||
'presetOptions' => [
|
||||
'identifier' => $nameObject . '_preset',
|
||||
'options' => $this->siteLanguagePresets->getAllForSelector(),
|
||||
],
|
||||
]);
|
||||
|
||||
$resultArray['html'] = $this->wrapWithFieldsetAndLegend($view->render('Form/SiteLanguageContainer'));
|
||||
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/form-engine/container/inline-control-container.js');
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?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\Container;
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
|
||||
/**
|
||||
* Render all tabs of a record that has tabs.
|
||||
*
|
||||
* This container is called from FullRecordContainer and resolves the --div-- structure,
|
||||
* operates on given fieldArrays and calls a PaletteAndSingleContainer for each single tab.
|
||||
*/
|
||||
class TabsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
// All the fields to handle in a flat list
|
||||
$fieldsArray = $this->data['fieldsArray'];
|
||||
|
||||
// Create a nested array from flat fieldArray list
|
||||
$tabsArray = [];
|
||||
// First element will be a --div--, so it is safe to start -1 here to trigger 0 as first array index
|
||||
$currentTabIndex = -1;
|
||||
foreach ($fieldsArray as $fieldString) {
|
||||
$fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
|
||||
if ($fieldArray['fieldName'] === '--div--') {
|
||||
$currentTabIndex++;
|
||||
if (empty($fieldArray['fieldLabel'])) {
|
||||
throw new \RuntimeException(
|
||||
'A --div-- has no label (--div--;fieldLabel) in showitem of ' . implode(',', $fieldsArray),
|
||||
1426454001
|
||||
);
|
||||
}
|
||||
$tabsArray[$currentTabIndex] = [
|
||||
'label' => $languageService->sL($fieldArray['fieldLabel']),
|
||||
'elements' => [],
|
||||
];
|
||||
} else {
|
||||
$tabsArray[$currentTabIndex]['elements'][] = $fieldArray;
|
||||
}
|
||||
}
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
$resultArray['javaScriptModules'][] = JavaScriptModuleInstruction::create('@typo3/backend/tab.js');
|
||||
|
||||
$domIdPrefix = 'DTM-' . md5($this->data['tableName'] . $this->data['databaseRow']['uid']);
|
||||
$tabCounter = 0;
|
||||
$tabElements = [];
|
||||
foreach ($tabsArray as $tabWithLabelAndElements) {
|
||||
$tabCounter++;
|
||||
$elements = $tabWithLabelAndElements['elements'];
|
||||
|
||||
// Merge elements of this tab into a single list again and hand over to
|
||||
// palette and single field container to render this group
|
||||
$options = $this->data;
|
||||
$options['tabAndInlineStack'][] = [
|
||||
'tab',
|
||||
$domIdPrefix . '-' . $tabCounter,
|
||||
];
|
||||
$options['fieldsArray'] = [];
|
||||
foreach ($elements as $element) {
|
||||
$options['fieldsArray'][] = implode(';', $element);
|
||||
}
|
||||
$options['renderType'] = 'paletteAndSingleContainer';
|
||||
$childArray = $this->nodeFactory->create($options)->render();
|
||||
|
||||
if ($childArray['html'] !== '') {
|
||||
$tabElements[] = [
|
||||
'label' => $tabWithLabelAndElements['label'],
|
||||
'content' => $childArray['html'],
|
||||
];
|
||||
}
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
|
||||
}
|
||||
|
||||
$resultArray['html'] = $this->renderTabMenu($tabElements, $domIdPrefix);
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 '*********';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to add custom controls to a TCA type="file" field in FormEngine
|
||||
*/
|
||||
final class CustomFileControlsEvent
|
||||
{
|
||||
private array $controls = [];
|
||||
|
||||
public function __construct(
|
||||
private array $resultArray,
|
||||
private readonly string $tableName,
|
||||
private readonly string $fieldName,
|
||||
private readonly array $databaseRow,
|
||||
private readonly array $fieldConfig,
|
||||
private readonly string $formFieldIdentifier,
|
||||
private readonly string $formFieldName,
|
||||
) {}
|
||||
|
||||
public function getResultArray(): array
|
||||
{
|
||||
return $this->resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* WARNING: Modifying the result array should be used with care. It mostly
|
||||
* only exists to allow additional $resultArray['javaScriptModules'].
|
||||
*/
|
||||
public function setResultArray(array $resultArray): void
|
||||
{
|
||||
$this->resultArray = $resultArray;
|
||||
}
|
||||
|
||||
public function getControls(): array
|
||||
{
|
||||
return $this->controls;
|
||||
}
|
||||
|
||||
public function setControls(array $controls): void
|
||||
{
|
||||
$this->controls = $controls;
|
||||
}
|
||||
|
||||
public function addControl(string $control, string $identifier = ''): void
|
||||
{
|
||||
if ($identifier !== '') {
|
||||
$this->controls[$identifier] = $control;
|
||||
} else {
|
||||
$this->controls[] = $control;
|
||||
}
|
||||
}
|
||||
|
||||
public function removeControl(string $identifier): bool
|
||||
{
|
||||
if (!isset($this->controls[$identifier])) {
|
||||
return false;
|
||||
}
|
||||
unset($this->controls[$identifier]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return $this->fieldName;
|
||||
}
|
||||
|
||||
public function getDatabaseRow(): array
|
||||
{
|
||||
return $this->databaseRow;
|
||||
}
|
||||
|
||||
public function getFieldConfig(): array
|
||||
{
|
||||
return $this->fieldConfig;
|
||||
}
|
||||
|
||||
public function getFormFieldIdentifier(): string
|
||||
{
|
||||
return $this->formFieldIdentifier;
|
||||
}
|
||||
|
||||
public function getFormFieldName(): string
|
||||
{
|
||||
return $this->formFieldName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\Filter\FileExtensionFilter;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to add custom file selectors to a
|
||||
* TCA type="file" field in FormEngine
|
||||
*/
|
||||
final class CustomFileSelectorsEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $selectors,
|
||||
private array $javascriptModules,
|
||||
private readonly string $tableName,
|
||||
private readonly string $fieldName,
|
||||
private readonly array $databaseRow,
|
||||
private readonly array $fieldConfig,
|
||||
private readonly FileExtensionFilter $fileExtensionFilter,
|
||||
private readonly string $formFieldIdentifier,
|
||||
) {}
|
||||
|
||||
public function getSelectors(): array
|
||||
{
|
||||
return $this->selectors;
|
||||
}
|
||||
|
||||
public function setSelectors(array $selectors): void
|
||||
{
|
||||
$this->selectors = $selectors;
|
||||
}
|
||||
|
||||
public function getJavascriptModules(): array
|
||||
{
|
||||
return $this->javascriptModules;
|
||||
}
|
||||
|
||||
public function setJavascriptModules(array $javascriptModules): void
|
||||
{
|
||||
$this->javascriptModules = $javascriptModules;
|
||||
}
|
||||
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
public function getFieldName(): string
|
||||
{
|
||||
return $this->fieldName;
|
||||
}
|
||||
|
||||
public function getDatabaseRow(): array
|
||||
{
|
||||
return $this->databaseRow;
|
||||
}
|
||||
|
||||
public function getFieldConfig(): array
|
||||
{
|
||||
return $this->fieldConfig;
|
||||
}
|
||||
|
||||
public function getFileExtensionFilter(): FileExtensionFilter
|
||||
{
|
||||
return $this->fileExtensionFilter;
|
||||
}
|
||||
|
||||
public function getFormFieldIdentifier(): string
|
||||
{
|
||||
return $this->formFieldIdentifier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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\Event;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedException;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to modify the user access
|
||||
* decision for using FormEngine to create or edit a record.
|
||||
*/
|
||||
final class ModifyEditFormUserAccessEvent
|
||||
{
|
||||
private bool $userHasAccess;
|
||||
|
||||
/**
|
||||
* @param 'new'|'edit' $command
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ?AccessDeniedException $exception,
|
||||
private readonly string $tableName,
|
||||
private readonly string $command,
|
||||
private readonly array $databaseRow,
|
||||
) {
|
||||
$this->userHasAccess = $this->exception === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows user access to the editing form
|
||||
*/
|
||||
public function allowUserAccess(): void
|
||||
{
|
||||
$this->userHasAccess = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Denies user access to the editing form
|
||||
*/
|
||||
public function denyUserAccess(): void
|
||||
{
|
||||
$this->userHasAccess = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current user access state
|
||||
*/
|
||||
public function doesUserHaveAccess(): bool
|
||||
{
|
||||
return $this->userHasAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* If Core's DataProvider previously denied access, this returns the corresponding
|
||||
* exception, `null` otherwise
|
||||
*/
|
||||
public function getAccessDeniedException(): ?AccessDeniedException
|
||||
{
|
||||
return $this->exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table name of the record in question
|
||||
*/
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the requested command, either `new` or `edit`
|
||||
* @return 'new'|'edit'
|
||||
*/
|
||||
public function getCommand(): string
|
||||
{
|
||||
return $this->command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the record's database row
|
||||
*/
|
||||
public function getDatabaseRow(): array
|
||||
{
|
||||
return $this->databaseRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to modify the controls
|
||||
* of a single file reference of a TCA type=file field.
|
||||
*/
|
||||
final class ModifyFileReferenceControlsEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $controls,
|
||||
private readonly array $data,
|
||||
private readonly array $record,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns all controls with their markup
|
||||
*/
|
||||
public function getControls(): array
|
||||
{
|
||||
return $this->controls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite the controls
|
||||
*/
|
||||
public function setControls(array $controls): void
|
||||
{
|
||||
$this->controls = $controls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the markup for the requested control
|
||||
*/
|
||||
public function getControl(string $identifier): string
|
||||
{
|
||||
return $this->controls[$identifier] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a control with the given identifier and markup
|
||||
* IMPORTANT: Overwrites an existing control with the same identifier
|
||||
*/
|
||||
public function setControl(string $identifier, string $markup): void
|
||||
{
|
||||
$this->controls[$identifier] = $markup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a control exists for the given identifier
|
||||
*/
|
||||
public function hasControl(string $identifier): bool
|
||||
{
|
||||
return isset($this->controls[$identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a control from the file reference, if it exists
|
||||
*
|
||||
* @return bool Whether the control could be removed
|
||||
*/
|
||||
public function removeControl(string $identifier): bool
|
||||
{
|
||||
if (!$this->hasControl($identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($this->controls[$identifier]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the whole element data
|
||||
*/
|
||||
public function getElementData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current record, the controls are created for
|
||||
*/
|
||||
public function getRecord(): array
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uid of the parent (embedding) record (uid or NEW...)
|
||||
*/
|
||||
public function getParentUid(): string
|
||||
{
|
||||
return (string)($this->data['inlineParentUid'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table (foreign_table) the controls are created for
|
||||
*/
|
||||
public function getForeignTable(): string
|
||||
{
|
||||
return (string)($this->getFieldConfiguration()['foreign_table'] ?? 'sys_file_reference');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TCA configuration of the TCA type=file field
|
||||
*/
|
||||
public function getFieldConfiguration(): array
|
||||
{
|
||||
return (array)($this->data['inlineParentConfig'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current records is only virtually shown and not physically part of the parent record
|
||||
*/
|
||||
public function isVirtual(): bool
|
||||
{
|
||||
return (bool)($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to modify the state (enabled or disabled) for controls of a file reference
|
||||
*/
|
||||
final class ModifyFileReferenceEnabledControlsEvent
|
||||
{
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private array $controlsState;
|
||||
|
||||
public function __construct(
|
||||
private readonly array $data,
|
||||
private readonly array $record,
|
||||
) {
|
||||
$this->controlsState = (array)($data['inlineParentConfig']['appearance']['enabledControls'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a control, if it exists
|
||||
*
|
||||
* @return bool Whether the control could be enabled
|
||||
*/
|
||||
public function enableControl(string $identifier): bool
|
||||
{
|
||||
if (!$this->hasControl($identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->controlsState[$identifier] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a control, if it exists
|
||||
*
|
||||
* @return bool Whether the control could be disabled
|
||||
*/
|
||||
public function disableControl(string $identifier): bool
|
||||
{
|
||||
if (!$this->hasControl($identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->controlsState[$identifier] = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a control exists for the given identifier
|
||||
*/
|
||||
public function hasControl(string $identifier): bool
|
||||
{
|
||||
return isset($this->controlsState[$identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the control is enabled.
|
||||
* Note: Will also return FALSE in case no control exists for the requested identifier
|
||||
*/
|
||||
public function isControlEnabled(string $identifier): bool
|
||||
{
|
||||
return (bool)($this->controlsState[$identifier] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all controls with their state (enabled or disabled)
|
||||
*/
|
||||
public function getControlsState(): array
|
||||
{
|
||||
return $this->controlsState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all enabled controls
|
||||
*/
|
||||
public function getEnabledControls(): array
|
||||
{
|
||||
return array_filter($this->controlsState, static fn(mixed $control): bool => (bool)$control === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the whole element data
|
||||
*/
|
||||
public function getElementData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current record of the controls are created for
|
||||
*/
|
||||
public function getRecord(): array
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uid of the parent (embedding) record (uid or NEW...)
|
||||
*/
|
||||
public function getParentUid(): string
|
||||
{
|
||||
return (string)($this->data['inlineParentUid'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table (foreign_table) the controls are created for
|
||||
*/
|
||||
public function getForeignTable(): string
|
||||
{
|
||||
return (string)($this->getFieldConfiguration()['foreign_table'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TCA configuration of the TCA type=file field
|
||||
*/
|
||||
public function getFieldConfiguration(): array
|
||||
{
|
||||
return (array)($this->data['inlineParentConfig'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current records is only virtually shown and not physically part of the parent record
|
||||
*/
|
||||
public function isVirtual(): bool
|
||||
{
|
||||
return (bool)($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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\Event;
|
||||
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to modify the preview url, used in the ImageManipulation element
|
||||
*/
|
||||
final class ModifyImageManipulationPreviewUrlEvent
|
||||
{
|
||||
private string $previewUrl = '';
|
||||
|
||||
public function __construct(
|
||||
private readonly array $databaseRow,
|
||||
private readonly array $fieldConfiguration,
|
||||
private readonly File $file,
|
||||
) {}
|
||||
|
||||
public function getDatabaseRow(): array
|
||||
{
|
||||
return $this->databaseRow;
|
||||
}
|
||||
|
||||
public function getFieldConfiguration(): array
|
||||
{
|
||||
return $this->fieldConfiguration;
|
||||
}
|
||||
|
||||
public function getFile(): File
|
||||
{
|
||||
return $this->file;
|
||||
}
|
||||
|
||||
public function getPreviewUrl(): string
|
||||
{
|
||||
return $this->previewUrl;
|
||||
}
|
||||
|
||||
public function setPreviewUrl(string $previewUrl): void
|
||||
{
|
||||
$this->previewUrl = $previewUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to modify the controls of an inline element
|
||||
*/
|
||||
final class ModifyInlineElementControlsEvent
|
||||
{
|
||||
public function __construct(
|
||||
private array $controls,
|
||||
private readonly array $data,
|
||||
private readonly array $record,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Returns all controls with their markup
|
||||
*/
|
||||
public function getControls(): array
|
||||
{
|
||||
return $this->controls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overwrite the controls
|
||||
*/
|
||||
public function setControls(array $controls): void
|
||||
{
|
||||
$this->controls = $controls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the markup for the requested control
|
||||
*/
|
||||
public function getControl(string $identifier): string
|
||||
{
|
||||
return $this->controls[$identifier] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a control with the given identifier and markup
|
||||
* IMPORTANT: Overwrites an existing control with the same identifier
|
||||
*/
|
||||
public function setControl(string $identifier, string $markup): void
|
||||
{
|
||||
$this->controls[$identifier] = $markup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a control exists for the given identifier
|
||||
*/
|
||||
public function hasControl(string $identifier): bool
|
||||
{
|
||||
return isset($this->controls[$identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a control from the inline element, if it exists
|
||||
*
|
||||
* @return bool Whether the control could be removed
|
||||
*/
|
||||
public function removeControl(string $identifier): bool
|
||||
{
|
||||
if (!$this->hasControl($identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
unset($this->controls[$identifier]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the whole element data
|
||||
*/
|
||||
public function getElementData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current record of the controls are created for
|
||||
*/
|
||||
public function getRecord(): array
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uid of the parent (embedding) record (uid or NEW...)
|
||||
*/
|
||||
public function getParentUid(): string
|
||||
{
|
||||
return (string)($this->data['inlineParentUid'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table (foreign_table) the controls are created for
|
||||
*/
|
||||
public function getForeignTable(): string
|
||||
{
|
||||
return (string)($this->getFieldConfiguration()['foreign_table'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TCA configuration of the inline record field
|
||||
*/
|
||||
public function getFieldConfiguration(): array
|
||||
{
|
||||
return (array)($this->data['inlineParentConfig'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current records is only virtually shown and not physically part of the parent record
|
||||
*/
|
||||
public function isVirtual(): bool
|
||||
{
|
||||
return (bool)($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to modify the state (enabled or disabled) for controls of an inline element
|
||||
*/
|
||||
final class ModifyInlineElementEnabledControlsEvent
|
||||
{
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private array $controlsState;
|
||||
|
||||
public function __construct(
|
||||
private readonly array $data,
|
||||
private readonly array $record,
|
||||
) {
|
||||
$this->controlsState = (array)($data['inlineParentConfig']['appearance']['enabledControls'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable a control, if it exists
|
||||
*
|
||||
* @return bool Whether the control could be enabled
|
||||
*/
|
||||
public function enableControl(string $identifier): bool
|
||||
{
|
||||
if (!$this->hasControl($identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->controlsState[$identifier] = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable a control, if it exists
|
||||
*
|
||||
* @return bool Whether the control could be disabled
|
||||
*/
|
||||
public function disableControl(string $identifier): bool
|
||||
{
|
||||
if (!$this->hasControl($identifier)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->controlsState[$identifier] = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a control exists for the given identifier
|
||||
*/
|
||||
public function hasControl(string $identifier): bool
|
||||
{
|
||||
return isset($this->controlsState[$identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the control is enabled.
|
||||
* Note: Will also return FALSE in case no control exists for the requested identifier
|
||||
*/
|
||||
public function isControlEnabled(string $identifier): bool
|
||||
{
|
||||
return (bool)($this->controlsState[$identifier] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all controls with their state (enabled or disabled)
|
||||
*/
|
||||
public function getControlsState(): array
|
||||
{
|
||||
return $this->controlsState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all enabled controls
|
||||
*/
|
||||
public function getEnabledControls(): array
|
||||
{
|
||||
return array_filter($this->controlsState, static fn(mixed $control): bool => (bool)$control === true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the whole element data
|
||||
*/
|
||||
public function getElementData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current record of the controls are created for
|
||||
*/
|
||||
public function getRecord(): array
|
||||
{
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uid of the parent (embedding) record (uid or NEW...)
|
||||
*/
|
||||
public function getParentUid(): string
|
||||
{
|
||||
return (string)($this->data['inlineParentUid'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table (foreign_table) the controls are created for
|
||||
*/
|
||||
public function getForeignTable(): string
|
||||
{
|
||||
return (string)($this->getFieldConfiguration()['foreign_table'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the TCA configuration of the inline record field
|
||||
*/
|
||||
public function getFieldConfiguration(): array
|
||||
{
|
||||
return (array)($this->data['inlineParentConfig'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the current records is only virtually shown and not physically part of the parent record
|
||||
*/
|
||||
public function isVirtual(): bool
|
||||
{
|
||||
return (bool)($this->data['isInlineDefaultLanguageRecordInLocalizedParentContext'] ?? false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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\Event;
|
||||
|
||||
/**
|
||||
* Listeners to this Event will be able to modify the link explanation array, used in FormEngine for link fields
|
||||
*/
|
||||
final class ModifyLinkExplanationEvent
|
||||
{
|
||||
private array $linkExplanation;
|
||||
|
||||
public function __construct(
|
||||
array $linkExplanation,
|
||||
private readonly array $linkData,
|
||||
private readonly array $linkParts,
|
||||
private readonly array $elementData,
|
||||
) {
|
||||
$this->linkExplanation = $linkExplanation;
|
||||
}
|
||||
|
||||
public function getLinkData(): array
|
||||
{
|
||||
return $this->linkData;
|
||||
}
|
||||
|
||||
public function getLinkParts(): array
|
||||
{
|
||||
return $this->linkParts;
|
||||
}
|
||||
|
||||
public function getElementData(): array
|
||||
{
|
||||
return $this->elementData;
|
||||
}
|
||||
|
||||
public function getLinkExplanation(): array
|
||||
{
|
||||
return $this->linkExplanation;
|
||||
}
|
||||
|
||||
public function setLinkExplanation(array $linkExplanation): void
|
||||
{
|
||||
$this->linkExplanation = $linkExplanation;
|
||||
}
|
||||
|
||||
public function getLinkExplanationValue(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->linkExplanation[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function setLinkExplanationValue(string $key, mixed $value): void
|
||||
{
|
||||
$this->linkExplanation[$key] = $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Generic backend form exception
|
||||
*/
|
||||
class Exception extends \TYPO3\CMS\Backend\Exception {}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* User is not allowed to edit this content elements
|
||||
*/
|
||||
class AccessDeniedContentEditException extends AccessDeniedException {}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* User check did not survive "checkRecordEditAccess" to edit record
|
||||
*/
|
||||
class AccessDeniedEditInternalsException extends AccessDeniedException {}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Exception;
|
||||
|
||||
/**
|
||||
* Access denied exception.
|
||||
* This indicated a recoverable error that should be changed to a user message.
|
||||
* This abstract exception is extended by more fine grained exceptions.
|
||||
*/
|
||||
abstract class AccessDeniedException extends Exception {}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* User access to record was denied by a ModifyRecordEditUserAccessEvent listener
|
||||
*/
|
||||
class AccessDeniedListenerException extends AccessDeniedException {}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* User is not allowed to edit this page
|
||||
*/
|
||||
class AccessDeniedPageEditException extends AccessDeniedException {}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* User is not allowed to add a new page
|
||||
*/
|
||||
class AccessDeniedPageNewException extends AccessDeniedException {}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* User has no sufficient rights to modify a row that is located at root node
|
||||
*/
|
||||
class AccessDeniedRootNodeException extends AccessDeniedException {}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?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\Exception;
|
||||
|
||||
/**
|
||||
* User has no modify table access to a table record
|
||||
*/
|
||||
class AccessDeniedTableModifyException extends AccessDeniedException {}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if a language overlay record is handled and no default language record is found.
|
||||
*/
|
||||
class DatabaseDefaultLanguageException extends Exception {}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Exception;
|
||||
|
||||
/**
|
||||
* A record could not be fetched from database, maybe it vanished meanwhile.
|
||||
*/
|
||||
class DatabaseRecordException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var string Table name
|
||||
*/
|
||||
protected $tableName;
|
||||
|
||||
/**
|
||||
* @var int Table row uid
|
||||
*/
|
||||
protected $uid;
|
||||
|
||||
/**
|
||||
* Constructor overwrites default constructor.
|
||||
*
|
||||
* @param string $message Human readable error message
|
||||
* @param int $code Exception code timestamp
|
||||
* @param \Exception|null $previousException Possible exception from database layer
|
||||
* @param string $tableName Table name query was working on
|
||||
* @param int $uid Table row uid
|
||||
*/
|
||||
public function __construct($message, $code, ?\Exception $previousException, string $tableName, int $uid)
|
||||
{
|
||||
parent::__construct($message, $code, $previousException);
|
||||
$this->tableName = $tableName;
|
||||
$this->uid = $uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return table name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTableName()
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return row uid
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUid()
|
||||
{
|
||||
return $this->uid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Exception;
|
||||
|
||||
/**
|
||||
* Thrown if a workspace delete placeholder is being edited, which is not allowed.
|
||||
*/
|
||||
class DatabaseRecordWorkspaceDeletePlaceholderException extends Exception
|
||||
{
|
||||
/**
|
||||
* @var string Table name
|
||||
*/
|
||||
protected string $tableName;
|
||||
|
||||
/**
|
||||
* @var int Table row uid
|
||||
*/
|
||||
protected int $uid;
|
||||
|
||||
/**
|
||||
* Constructor overwrites default constructor.
|
||||
*
|
||||
* @param string $message Human readable error message
|
||||
* @param int $code Exception code timestamp
|
||||
* @param string $tableName Table name query was working on
|
||||
* @param int $uid Table row uid
|
||||
*/
|
||||
public function __construct(string $message, int $code, string $tableName, int $uid)
|
||||
{
|
||||
parent::__construct($message, $code);
|
||||
$this->tableName = $tableName;
|
||||
$this->uid = $uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return table name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTableName()
|
||||
{
|
||||
return $this->tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return row uid
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUid()
|
||||
{
|
||||
return $this->uid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Exception;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Exception;
|
||||
|
||||
/**
|
||||
* No fields to be rendered for the requested record
|
||||
*/
|
||||
class NoFieldsToRenderException extends Exception {}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?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\FieldControl;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Renders the icon with link parameters to add a new record,
|
||||
* typically used for single elements of type=group or type=select.
|
||||
*/
|
||||
class AddRecord extends AbstractNode
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add button control
|
||||
*
|
||||
* @return array As defined by FieldControl class
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$options = $this->data['renderData']['fieldControlOptions'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$itemName = (string)$parameterArray['itemFormElName'];
|
||||
|
||||
// Handle options and fallback
|
||||
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNew';
|
||||
$setValue = $options['setValue'] ?? 'append';
|
||||
|
||||
$table = '';
|
||||
if (isset($options['table'])) {
|
||||
// Table given in options - use it
|
||||
$table = $options['table'];
|
||||
} elseif ($parameterArray['fieldConf']['config']['type'] === 'group'
|
||||
&& !empty($parameterArray['fieldConf']['config']['allowed'])
|
||||
) {
|
||||
// Use first table from allowed list if specific table is not set in options
|
||||
$allowedTables = GeneralUtility::trimExplode(',', $parameterArray['fieldConf']['config']['allowed'], true);
|
||||
$table = array_pop($allowedTables);
|
||||
} elseif ($parameterArray['fieldConf']['config']['type'] === 'select'
|
||||
&& !empty($parameterArray['fieldConf']['config']['foreign_table'])
|
||||
) {
|
||||
// Use foreign_table if given for type=select
|
||||
$table = $parameterArray['fieldConf']['config']['foreign_table'];
|
||||
}
|
||||
if (empty($table)) {
|
||||
// Still no table - this element can not handle the add control.
|
||||
return [];
|
||||
}
|
||||
|
||||
$prefixOfFormElName = 'data[' . $this->data['tableName'] . '][' . $this->data['databaseRow']['uid'] . '][' . $this->data['fieldName'] . ']';
|
||||
$flexFormPath = '';
|
||||
if (str_starts_with($itemName, $prefixOfFormElName)) {
|
||||
$flexFormPath = str_replace('][', '/', substr($itemName, strlen($prefixOfFormElName) + 1, -1));
|
||||
}
|
||||
|
||||
$urlParameters = [
|
||||
'P' => [
|
||||
'params' => [
|
||||
'table' => $table,
|
||||
'pid' => $this->resolvePid($options, $table),
|
||||
'setValue' => $setValue,
|
||||
],
|
||||
'table' => $this->data['tableName'],
|
||||
'field' => $this->data['fieldName'],
|
||||
'uid' => $this->data['databaseRow']['uid'],
|
||||
'flexFormPath' => $flexFormPath,
|
||||
'returnUrl' => $this->data['returnUrl'],
|
||||
],
|
||||
];
|
||||
|
||||
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
|
||||
|
||||
return [
|
||||
'iconIdentifier' => 'actions-plus',
|
||||
'title' => $title,
|
||||
'linkAttributes' => [
|
||||
'id' => $id,
|
||||
'href' => (string)$this->uriBuilder->buildUriFromRoute('wizard_add', $urlParameters),
|
||||
],
|
||||
'javaScriptModules' => [
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-control/add-record.js')->instance('#' . $id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function resolvePid(array $options, string $table): string
|
||||
{
|
||||
// Target pid of new records is current pid by default
|
||||
$pid = $this->data['effectivePid'];
|
||||
if (isset($options['pid'])) {
|
||||
// pid configured in options - use it
|
||||
if ($options['pid'] === '###SITEROOT###' && ($this->data['site'] ?? null) instanceof Site) {
|
||||
// Substitute marker with pid from site object
|
||||
$pid = $this->data['site']->getRootPageId();
|
||||
} else {
|
||||
// This might be a static pid or a marker such as ###PAGE_TSCONFIG_ID###
|
||||
$pid = $options['pid'];
|
||||
}
|
||||
} elseif (
|
||||
$this->data['tcaSchemata']->has($table)
|
||||
&& (int)($this->data['tcaSchemata']->get($table)->getRawConfiguration()['rootLevel'] ?? 0) === 1
|
||||
) {
|
||||
// Target table can only exist on root level - set 0 as pid
|
||||
$pid = 0;
|
||||
}
|
||||
return (string)$pid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?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\FieldControl;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\Form\Behavior\OnFieldChangeTrait;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Renders the icon with link parameters to edit a selected element,
|
||||
* typically used for single elements of type=group or type=select.
|
||||
*/
|
||||
class EditPopup extends AbstractNode
|
||||
{
|
||||
use OnFieldChangeTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
private readonly HashService $hashService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Edit popup control
|
||||
*
|
||||
* @return array As defined by FieldControl class
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$options = $this->data['renderData']['fieldControlOptions'];
|
||||
|
||||
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.edit';
|
||||
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$itemName = $parameterArray['itemFormElName'];
|
||||
$windowOpenParameters = $options['windowOpenParameters'] ?? 'height=800,width=900,status=0,menubar=0,scrollbars=1';
|
||||
|
||||
$flexFormDataStructureIdentifier = $this->data['flexFormDataStructureIdentifier'] ?? '';
|
||||
$flexFormDataStructurePath = '';
|
||||
if (!empty($flexFormDataStructureIdentifier)) {
|
||||
if (empty($this->data['flexFormContainerName'])) {
|
||||
// simple flex form element
|
||||
$flexFormDataStructurePath = 'sheets/'
|
||||
. $this->data['flexFormSheetName']
|
||||
. '/ROOT/el/'
|
||||
. $this->data['flexFormFieldName']
|
||||
. '/config';
|
||||
} else {
|
||||
// flex form section container element
|
||||
$flexFormDataStructurePath = 'sheets/'
|
||||
. $this->data['flexFormSheetName']
|
||||
. '/ROOT/el/'
|
||||
. $this->data['flexFormFieldName']
|
||||
. '/el/'
|
||||
. $this->data['flexFormContainerName']
|
||||
. '/el/'
|
||||
. $this->data['flexFormContainerFieldName']
|
||||
. '/config';
|
||||
}
|
||||
}
|
||||
|
||||
$urlParameters = array_merge(
|
||||
[
|
||||
'table' => $this->data['tableName'],
|
||||
'field' => $this->data['fieldName'],
|
||||
'formName' => 'editform',
|
||||
'flexFormDataStructureIdentifier' => $flexFormDataStructureIdentifier,
|
||||
'flexFormDataStructurePath' => $flexFormDataStructurePath,
|
||||
'hmac' => $this->hashService->hmac('editform' . $itemName, 'wizard_js'),
|
||||
],
|
||||
$this->forwardOnFieldChangeQueryParams($parameterArray['fieldChangeFunc'] ?? [])
|
||||
);
|
||||
$url = (string)$this->uriBuilder->buildUriFromRoute('wizard_edit', ['P' => $urlParameters]);
|
||||
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
|
||||
|
||||
return [
|
||||
'iconIdentifier' => 'actions-open',
|
||||
'title' => $title,
|
||||
'linkAttributes' => [
|
||||
'id' => $id,
|
||||
'href' => $url,
|
||||
'data-element' => $itemName,
|
||||
'data-window-parameters' => $windowOpenParameters,
|
||||
],
|
||||
'javaScriptModules' => [
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-control/edit-popup.js')->instance('#' . $id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
<?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\FieldControl;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
|
||||
use TYPO3\CMS\Core\Site\Entity\Site;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Renders the icon "select element via element browser",
|
||||
* typically used for type=group.
|
||||
*/
|
||||
class ElementBrowser extends AbstractNode
|
||||
{
|
||||
public function __construct(
|
||||
private readonly InlineStackProcessor $inlineStackProcessor,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add button control
|
||||
*
|
||||
* @return array As defined by FieldControl class
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$table = $this->data['tableName'];
|
||||
$fieldName = $this->data['fieldName'];
|
||||
$options = $this->data['renderData']['fieldControlOptions'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$elementName = $parameterArray['itemFormElName'];
|
||||
$config = $parameterArray['fieldConf']['config'];
|
||||
$type = $config['type'];
|
||||
|
||||
// Remove any white-spaces from the allowed extension lists
|
||||
$allowed = implode(',', GeneralUtility::trimExplode(',', (string)($config['allowed'] ?? ''), true));
|
||||
|
||||
if (isset($config['readOnly']) && $config['readOnly']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($options['title'] ?? false) {
|
||||
$title = $options['title'];
|
||||
} elseif ($type === 'group') {
|
||||
$title = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_db';
|
||||
} elseif ($type === 'folder') {
|
||||
$title = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_folder';
|
||||
} else {
|
||||
// FieldControl requires to provide a title -> Set default if non is given and custom TCA config is used
|
||||
$title = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_elements';
|
||||
}
|
||||
|
||||
// Check against inline uniqueness - Create some onclick js for delete control and element browser
|
||||
// to override record selection in some FAL scenarios - See 'appearance' docs of group element
|
||||
$objectPrefix = '';
|
||||
if (($this->data['isInlineChild'] ?? false)
|
||||
&& ($this->data['inlineParentUid'] ?? false)
|
||||
&& ($this->data['inlineParentConfig']['foreign_table'] ?? false) === $table
|
||||
&& ($this->data['inlineParentConfig']['foreign_unique'] ?? false) === $fieldName
|
||||
) {
|
||||
$objectPrefix = $this->inlineStackProcessor->getDomObjectIdPrefixFromStructure($this->data['inlineStructure'], $this->data['inlineFirstPid']) . '-' . $table;
|
||||
}
|
||||
|
||||
if ($type === 'group') {
|
||||
if (($this->data['inlineParentConfig']['type'] ?? '') === 'file' || ($config['allowed'] ?? '') === 'sys_file') {
|
||||
$elementBrowserType = 'file';
|
||||
// Remove any white-spaces from the allowed extension lists
|
||||
$allowed = implode(',', GeneralUtility::trimExplode(',', (string)($this->data['inlineParentConfig']['allowed'] ?? ''), true));
|
||||
} else {
|
||||
$elementBrowserType = 'db';
|
||||
}
|
||||
} else {
|
||||
$elementBrowserType = 'folder';
|
||||
}
|
||||
|
||||
// Initialize link attributes
|
||||
$linkAttributes = [
|
||||
'class' => 't3js-element-browser',
|
||||
'data-mode' => $elementBrowserType,
|
||||
'data-field-reference' => $elementName,
|
||||
'data-allowed-types' => $allowed,
|
||||
'data-irre-object-id' => $objectPrefix,
|
||||
'data-use-events' => 'true',
|
||||
];
|
||||
|
||||
// Add the default entry point - if found
|
||||
$linkAttributes = $this->addEntryPoint($table, $fieldName, $config, $linkAttributes);
|
||||
|
||||
return [
|
||||
'iconIdentifier' => 'actions-insert-record',
|
||||
'title' => $title,
|
||||
'linkAttributes' => $linkAttributes,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve a configured default entry point - page / folder
|
||||
* to be expanded - and add it to the link attributes if found.
|
||||
*/
|
||||
protected function addEntryPoint(string $table, string $fieldName, array $fieldConfig, array $linkAttributes): array
|
||||
{
|
||||
if (!isset($fieldConfig['elementBrowserEntryPoints']) || !is_array($fieldConfig['elementBrowserEntryPoints'])) {
|
||||
// Early return in case no entry points are defined
|
||||
return $linkAttributes;
|
||||
}
|
||||
|
||||
// Fetch the configured default entry point (which might be a marker)
|
||||
$entryPoint = (string)($fieldConfig['elementBrowserEntryPoints']['_default'] ?? '');
|
||||
|
||||
// In case no default entry point is given, check if we deal with type=db and only one allowed table
|
||||
if ($entryPoint === '') {
|
||||
if ($fieldConfig['type'] === 'folder') {
|
||||
// Return for type folder as this requires the "_default" key to be set
|
||||
return $linkAttributes;
|
||||
}
|
||||
// Check for the allowed tables, if only one table is allowed check if an entry point is defined for it
|
||||
$allowed = GeneralUtility::trimExplode(',', $fieldConfig['allowed'] ?? '', true);
|
||||
if (count($allowed) === 1 && isset($fieldConfig['elementBrowserEntryPoints'][$allowed[0]])) {
|
||||
// Use the entry point for the single table as default
|
||||
$entryPoint = (string)$fieldConfig['elementBrowserEntryPoints'][$allowed[0]];
|
||||
}
|
||||
if ($entryPoint === '') {
|
||||
// Return if still empty
|
||||
return $linkAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
// Check and resolve possible marker
|
||||
if (str_starts_with($entryPoint, '###') && str_ends_with($entryPoint, '###')) {
|
||||
if ($entryPoint === '###CURRENT_PID###') {
|
||||
// Use the current pid
|
||||
$entryPoint = (string)$this->data['effectivePid'];
|
||||
} elseif ($entryPoint === '###SITEROOT###' && ($this->data['site'] ?? null) instanceof Site) {
|
||||
// Use the root page id from the current site
|
||||
$entryPoint = (string)$this->data['site']->getRootPageId();
|
||||
} else {
|
||||
// Check for special TSconfig marker
|
||||
$TSconfig = FormEngineUtility::getTCEFORM_TSconfig($table, ['pid' => $this->data['effectivePid']]);
|
||||
$keyword = substr($entryPoint, 3, -3);
|
||||
if (str_starts_with($keyword, 'PAGE_TSCONFIG_')) {
|
||||
$entryPoint = (string)($TSconfig[$fieldName][$keyword] ?? '');
|
||||
} else {
|
||||
$entryPoint = (string)($TSconfig['_' . $keyword] ?? '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the entry point to the link attribute - if resolved
|
||||
if ($entryPoint !== '') {
|
||||
$linkAttributes['data-entry-point'] = $entryPoint;
|
||||
}
|
||||
|
||||
return $linkAttributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?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\FieldControl;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Renders the icon "insert record from clipboard",
|
||||
* typically used for type=group.
|
||||
*/
|
||||
class InsertClipboard extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* Add button control
|
||||
*
|
||||
* @return array As defined by FieldControl class
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$elementName = $parameterArray['itemFormElName'];
|
||||
$config = $parameterArray['fieldConf']['config'];
|
||||
$clipboardElements = $config['clipboardElements'];
|
||||
|
||||
if ((isset($config['readOnly']) && $config['readOnly'])
|
||||
|| empty($clipboardElements)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$dataAttributes = [
|
||||
'element' => $elementName,
|
||||
'clipboardItems' => [],
|
||||
];
|
||||
$title = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clipInsert_db'), count($clipboardElements));
|
||||
foreach ($clipboardElements as $clipboardElement) {
|
||||
$dataAttributes['clipboardItems'][] = [
|
||||
'title' => $clipboardElement['title'],
|
||||
'value' => $clipboardElement['value'],
|
||||
];
|
||||
}
|
||||
|
||||
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
|
||||
|
||||
return [
|
||||
'iconIdentifier' => 'actions-document-paste-into',
|
||||
'title' => $title,
|
||||
'linkAttributes' => [
|
||||
'id' => $id,
|
||||
'data-element' => $dataAttributes['element'],
|
||||
'data-clipboard-items' => json_encode($dataAttributes['clipboardItems']),
|
||||
],
|
||||
'javaScriptModules' => [
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-control/insert-clipboard.js')->instance('#' . $id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?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\FieldControl;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\Form\Behavior\OnFieldChangeTrait;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Crypto\HashService;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Renders the icon with link parameters to open the element browser.
|
||||
* Used in InputLinkElement.
|
||||
*/
|
||||
class LinkPopup extends AbstractNode
|
||||
{
|
||||
use OnFieldChangeTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
private readonly HashService $hashService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Link popup control
|
||||
*
|
||||
* @return array As defined by FieldControl class
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$options = $this->data['renderData']['fieldControlOptions'];
|
||||
|
||||
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.link';
|
||||
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$itemName = $parameterArray['itemFormElName'];
|
||||
|
||||
$linkBrowserArguments = [];
|
||||
if (is_array($options['allowedTypes'] ?? false)) {
|
||||
$linkBrowserArguments['allowedTypes'] = implode(',', $options['allowedTypes']);
|
||||
} elseif (isset($options['blindLinkOptions'])) {
|
||||
// @todo Deprecate this option
|
||||
$linkBrowserArguments['blindLinkOptions'] = $options['blindLinkOptions'];
|
||||
}
|
||||
if (is_array($options['allowedOptions'] ?? false)) {
|
||||
$linkBrowserArguments['allowedOptions'] = implode(',', $options['allowedOptions']);
|
||||
} elseif (isset($options['blindLinkFields'])) {
|
||||
// @todo Deprecate this option
|
||||
$linkBrowserArguments['blindLinkFields'] = $options['blindLinkFields'];
|
||||
}
|
||||
if (is_array($options['allowedFileExtensions'] ?? false)) {
|
||||
$linkBrowserArguments['allowedFileExtensions'] = implode(',', $options['allowedFileExtensions']);
|
||||
} elseif (isset($options['allowedExtensions'])) {
|
||||
// @todo Deprecate this option
|
||||
$linkBrowserArguments['allowedExtensions'] = $options['allowedExtensions'];
|
||||
}
|
||||
$urlParameters = array_merge(
|
||||
[
|
||||
'params' => $linkBrowserArguments,
|
||||
'table' => $this->data['tableName'],
|
||||
'uid' => $this->data['databaseRow']['uid'],
|
||||
'pid' => $this->data['databaseRow']['pid'],
|
||||
'field' => $this->data['fieldName'],
|
||||
'formName' => 'editform',
|
||||
'itemName' => $itemName,
|
||||
'hmac' => $this->hashService->hmac('editform' . $itemName, 'wizard_js'),
|
||||
],
|
||||
$this->forwardOnFieldChangeQueryParams($parameterArray['fieldChangeFunc'] ?? [])
|
||||
);
|
||||
$url = (string)$this->uriBuilder->buildUriFromRoute('wizard_link', ['P' => $urlParameters]);
|
||||
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
|
||||
$label = $this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_browse_links.xlf:openLinkWizard');
|
||||
return [
|
||||
'iconIdentifier' => 'actions-wizard-link',
|
||||
'title' => $title,
|
||||
'linkAttributes' => [
|
||||
'id' => $id,
|
||||
'href' => $url,
|
||||
'data-item-name' => $itemName,
|
||||
'aria-label' => $label,
|
||||
],
|
||||
'javaScriptModules' => [
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-control/link-popup.js')->instance('#' . $id),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?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\FieldControl;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Renders the icon with link parameters to jump to the records module
|
||||
* "single table" view, showing only one configurable table.
|
||||
*/
|
||||
class ListModule extends AbstractNode
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UriBuilder $uriBuilder,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Add button control
|
||||
*
|
||||
* @return array As defined by FieldControl class
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$options = $this->data['renderData']['fieldControlOptions'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
|
||||
// Handle options and fallback
|
||||
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.list';
|
||||
|
||||
$table = '';
|
||||
if (isset($options['table'])) {
|
||||
// Table given in options - use it
|
||||
$table = $options['table'];
|
||||
} elseif ($parameterArray['fieldConf']['config']['type'] === 'group'
|
||||
&& !empty($parameterArray['fieldConf']['config']['allowed'])
|
||||
) {
|
||||
// Use first table from allowed list if specific table is not set in options
|
||||
$allowedTables = GeneralUtility::trimExplode(',', $parameterArray['fieldConf']['config']['allowed'], true);
|
||||
$table = array_pop($allowedTables);
|
||||
} elseif ($parameterArray['fieldConf']['config']['type'] === 'select'
|
||||
&& !empty($parameterArray['fieldConf']['config']['foreign_table'])
|
||||
) {
|
||||
// Use foreign_table if given for type=select
|
||||
$table = $parameterArray['fieldConf']['config']['foreign_table'];
|
||||
}
|
||||
if (empty($table)) {
|
||||
// Still no table - this element can not handle the list control.
|
||||
return [];
|
||||
}
|
||||
|
||||
// Target pid of new records is current pid by default
|
||||
$pid = $this->data['effectivePid'];
|
||||
if (isset($options['pid'])) {
|
||||
// pid configured in options - use it
|
||||
$pid = $options['pid'];
|
||||
} elseif (
|
||||
$this->data['tcaSchemata']->has($table)
|
||||
&& ($this->data['tcaSchemata']->get($table)->getRawConfiguration()['rootLevel'] ?? false) === 1
|
||||
) {
|
||||
// Target table can only exist on root level - set 0 as pid
|
||||
$pid = 0;
|
||||
}
|
||||
|
||||
$urlParameters = [
|
||||
'P' => [
|
||||
'params' => [
|
||||
'table' => $table,
|
||||
'pid' => $pid,
|
||||
],
|
||||
'table' => $this->data['tableName'],
|
||||
'field' => $this->data['fieldName'],
|
||||
'uid' => $this->data['databaseRow']['uid'],
|
||||
'returnUrl' => $this->data['returnUrl'],
|
||||
],
|
||||
];
|
||||
|
||||
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
|
||||
|
||||
return [
|
||||
'iconIdentifier' => 'actions-list-alternative',
|
||||
'title' => $title,
|
||||
'linkAttributes' => [
|
||||
'id' => $id,
|
||||
'href' => (string)$this->uriBuilder->buildUriFromRoute('wizard_list', $urlParameters),
|
||||
],
|
||||
'javaScriptModules' => [
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-control/list-module.js')->instance('#' . $id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?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\FieldControl;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Renders a widget to generate a random string in JavaScript.
|
||||
*
|
||||
* This is typically used in combination with TCA type=password as password
|
||||
* generator, but can be potentially used with other field input types as well.
|
||||
*
|
||||
* @internal This is still a bit experimental and may change, for instance to
|
||||
* be combined with passwordPolicies.
|
||||
*/
|
||||
class PasswordGenerator extends AbstractNode
|
||||
{
|
||||
public function render(): array
|
||||
{
|
||||
$options = $this->data['renderData']['fieldControlOptions'];
|
||||
$itemName = (string)$this->data['parameterArray']['itemFormElName'];
|
||||
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
|
||||
|
||||
// Handle options and fallback
|
||||
$title = $options['title'] ?? 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.generatePassword';
|
||||
|
||||
$linkAttributes = [
|
||||
'id' => $id,
|
||||
'data-item-name' => $itemName,
|
||||
];
|
||||
|
||||
if ($options['allowEdit'] ?? true) {
|
||||
$linkAttributes['data-allow-edit'] = true;
|
||||
}
|
||||
|
||||
if (is_string($options['passwordPolicy'] ?? null) && $options['passwordPolicy'] !== '') {
|
||||
$linkAttributes['data-password-policy'] = $options['passwordPolicy'];
|
||||
}
|
||||
|
||||
return [
|
||||
'iconIdentifier' => 'actions-dice',
|
||||
'title' => $title,
|
||||
'linkAttributes' => $linkAttributes,
|
||||
'javaScriptModules' => [
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-control/password-generator.js')->instance($id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?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\FieldControl;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* "Reset selection to previous selected items" icon,
|
||||
* typically used by type=select with renderType=selectSingleBox
|
||||
*/
|
||||
class ResetSelection extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* Add button control
|
||||
*
|
||||
* @return array As defined by FieldControl class
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$selectItems = $parameterArray['fieldConf']['config']['items'];
|
||||
if (($parameterArray['fieldConf']['config']['readOnly'] ?? false) || empty($selectItems)) {
|
||||
// Early return if the field is readOnly or no items exist
|
||||
return [];
|
||||
}
|
||||
$itemName = $parameterArray['itemFormElName'];
|
||||
$itemArray = array_flip($parameterArray['itemFormElValue']);
|
||||
$initiallySelectedIndices = [];
|
||||
foreach ($selectItems as $i => $item) {
|
||||
$value = $item['value'];
|
||||
// Selected or not by default
|
||||
if (isset($itemArray[$value])) {
|
||||
$initiallySelectedIndices[] = $i;
|
||||
}
|
||||
}
|
||||
|
||||
$id = StringUtility::getUniqueId('t3js-formengine-fieldcontrol-');
|
||||
|
||||
return [
|
||||
'iconIdentifier' => 'actions-edit-undo',
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.revertSelection',
|
||||
'linkAttributes' => [
|
||||
'id' => $id,
|
||||
'data-item-name' => $itemName,
|
||||
'data-selected-indices' => json_encode($initiallySelectedIndices),
|
||||
],
|
||||
'javaScriptModules' => [
|
||||
JavaScriptModuleInstruction::create('@typo3/backend/form-engine/field-control/reset-selection.js')->instance('#' . $id),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?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\FieldInformation;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
|
||||
/**
|
||||
* This field information node is used in be_user admin field
|
||||
* to show some additional information if the edited user
|
||||
* is a system maintainer or becomes one if togging the admin flag.
|
||||
*/
|
||||
class AdminIsSystemMaintainer extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* Handler for single nodes
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
if ($this->data['tableName'] !== 'be_users' || $this->data['fieldName'] !== 'admin') {
|
||||
throw new \RuntimeException(
|
||||
'The adminIsSystemMaintainer field information can be used for admin field in be_users table only',
|
||||
1537273550
|
||||
);
|
||||
}
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
if ($this->data['command'] === 'new') {
|
||||
// Early return on 'new' records - nothing we can do here
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$systemMaintainers = array_map(intval(...), $GLOBALS['TYPO3_CONF_VARS']['SYS']['systemMaintainers'] ?? []);
|
||||
$isTargetUserInSystemMaintainerList = in_array((int)$this->data['vanillaUid'], $systemMaintainers, true);
|
||||
if ($isTargetUserInSystemMaintainerList) {
|
||||
$languageService = $this->getLanguageService();
|
||||
$isTargetUserAdmin = (int)$this->data['databaseRow']['admin'] === 1;
|
||||
if ($isTargetUserAdmin) {
|
||||
// User is a system maintainer
|
||||
$fieldInformationText = '<strong>' . htmlspecialchars($languageService->sL(
|
||||
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:formEngine.beUser.admin.information.userIsSystemMaintainer'
|
||||
)) . '</strong>';
|
||||
} else {
|
||||
// User is currently not an admin, but set as system maintainer (in-effective).
|
||||
// If admin field is set to 1, the user is therefore system maintainer again.
|
||||
$fieldInformationText = '<strong>' . htmlspecialchars($languageService->sL(
|
||||
'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:formEngine.beUser.admin.information.userWillBecomeSystemMaintainer'
|
||||
)) . '</strong>';
|
||||
}
|
||||
$resultArray['html'] = $fieldInformationText;
|
||||
}
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?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\FieldInformation;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
|
||||
/**
|
||||
* This field information node is used for the pages backend_layout
|
||||
* field to inform about a possible backend layout, inherited form
|
||||
* a parent page.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class BackendLayoutFromParentPage extends AbstractNode
|
||||
{
|
||||
public function __construct(
|
||||
private readonly BackendLayoutView $backendLayoutView,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Handler for single nodes
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
if ($this->data['tableName'] !== 'pages' || $this->data['fieldName'] !== 'backend_layout') {
|
||||
throw new \RuntimeException(
|
||||
'The backendLayoutFromParentPage field information can only be used for the backend_layout field of the pages table.',
|
||||
1622109821
|
||||
);
|
||||
}
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
|
||||
// In case the backend_layout field of the current page is not empty, no backend layout will be inherited.
|
||||
if (!empty($parameterArray['itemFormElValue'])) {
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$backendLayoutInformation = '';
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
if ($this->data['command'] === 'new') {
|
||||
// In case we deal with a new record, we try to find a possible inherited backend layout in
|
||||
// the rootline. Since there might be further actions, e.g. DataHandler hooks, the actually
|
||||
// resolved backend layout can only be determined, once the record is saved. For now we just
|
||||
// inform about the backend layout, which will most likely be used.
|
||||
foreach ($this->data['rootline'] as $page) {
|
||||
if (!empty($page['backend_layout_next_level']) && ($page['uid'] ?? false)) {
|
||||
$backendLayoutInformation = sprintf(
|
||||
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:formEngine.pages.backendLayout.information.inheritFromParentPage'),
|
||||
$this->getFieldValueLabel($parameterArray['fieldConf'], $page['backend_layout_next_level'])
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get the resolved backend layout for the current page.
|
||||
$backendLayout = $this->backendLayoutView->getBackendLayoutForPage((int)($this->data['databaseRow']['uid'] ?? $this->data['effectivePid'] ?? 0));
|
||||
$backendLayoutInformation = sprintf(
|
||||
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:formEngine.pages.backendLayout.information.inheritedFromParentPage'),
|
||||
$languageService->sL($backendLayout->getTitle())
|
||||
);
|
||||
}
|
||||
|
||||
if ($backendLayoutInformation !== '') {
|
||||
$resultArray['html'] = '<p class="text-variant">' . htmlspecialchars($backendLayoutInformation) . '</p>';
|
||||
}
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
protected function getFieldValueLabel(array $fieldConfiguration, string $fieldValue): string
|
||||
{
|
||||
foreach ($fieldConfiguration['config']['items'] as $item) {
|
||||
if (($item['value'] ?? '') === $fieldValue && !empty($item['label'])) {
|
||||
return $item['label'];
|
||||
}
|
||||
}
|
||||
|
||||
$invalidValue = sprintf(
|
||||
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.noMatchingValue'),
|
||||
$fieldValue
|
||||
);
|
||||
|
||||
return '[ ' . $invalidValue . ' ]';
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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\FieldInformation;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
|
||||
/**
|
||||
* Renders an info badge indicating that no selectable items are available
|
||||
* for a relational field. Shown when backend debug mode is enabled.
|
||||
*/
|
||||
class NoSelectableItemsAvailable extends AbstractNode
|
||||
{
|
||||
public function render(): array
|
||||
{
|
||||
$resultArray = $this->initializeResultArray();
|
||||
$text = htmlspecialchars($this->getLanguageService()->sL(
|
||||
'core.core:labels.noSelectableItemsAvailable'
|
||||
));
|
||||
$resultArray['html'] = '<div class="mb-2"><span class="badge badge-info">' . $text . '</span></div>';
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
private function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\FieldWizard;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\DiffUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Renders the diff-view of default language record content compared with what the record was originally
|
||||
* translated from. Will render content if any is found in the internal array.
|
||||
*
|
||||
* This is typically used of renderTypes that are based on text input
|
||||
*/
|
||||
class DefaultLanguageDifferences extends AbstractNode
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DiffUtility $diffUtility,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Render the diff view if enabled
|
||||
*
|
||||
* @return array Result array
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$result = $this->initializeResultArray();
|
||||
|
||||
$row = $this->data['databaseRow'];
|
||||
$table = $this->data['tableName'];
|
||||
$fieldName = $this->data['fieldName'];
|
||||
$fieldConfig = $this->data['processedTca']['columns'][$fieldName];
|
||||
$l10nDisplay = $this->data['parameterArray']['fieldConf']['l10n_display'] ?? '';
|
||||
$defaultLanguageRow = $this->data['defaultLanguageRow'] ?? null;
|
||||
$defaultLanguageDiffRow = $this->data['defaultLanguageDiffRow'][$table . ':' . $row['uid']] ?? null;
|
||||
|
||||
if (!is_array($defaultLanguageDiffRow)
|
||||
|| GeneralUtility::inList($l10nDisplay, 'hideDiff')
|
||||
|| GeneralUtility::inList($l10nDisplay, 'defaultAsReadonly')
|
||||
|| !isset($defaultLanguageDiffRow[$fieldName])
|
||||
|| $fieldConfig['config']['type'] === 'inline'
|
||||
|| $fieldConfig['config']['type'] === 'file'
|
||||
|| $fieldConfig['config']['type'] === 'flex'
|
||||
) {
|
||||
// Early return if there is no diff row or if display is disabled
|
||||
return $result;
|
||||
}
|
||||
|
||||
$languageService = $this->getLanguageService();
|
||||
$html = [];
|
||||
if ((string)$defaultLanguageDiffRow[$fieldName] !== (string)$defaultLanguageRow[$fieldName]) {
|
||||
// Create diff-result:
|
||||
$diffResult = $this->diffUtility->diff(
|
||||
(string)BackendUtility::getProcessedValue($table, $fieldName, $defaultLanguageDiffRow[$fieldName], 0, true, false, 0, true, 0, $defaultLanguageRow),
|
||||
(string)BackendUtility::getProcessedValue($table, $fieldName, $defaultLanguageRow[$fieldName], 0, true, false, 0, true, 0, $defaultLanguageDiffRow)
|
||||
);
|
||||
$html[] = '<div class="t3-form-original-language-diff">';
|
||||
$html[] = '<div class="t3-form-original-language-diffheader">';
|
||||
$html[] = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.changeInOrig'));
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div class="t3-form-original-language-diffcontent">';
|
||||
$html[] = '<div class="diff">';
|
||||
$html[] = '<div class="diff-item">';
|
||||
$html[] = '<div class="diff-item-result diff-item-result-inline">' . $diffResult . '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
}
|
||||
$result['html'] = implode(LF, $html);
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
<?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\FieldWizard;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Core\DataHandling\Localization\State;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Page\JavaScriptModuleInstruction;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Allows to define the localization state per field.
|
||||
*/
|
||||
class LocalizationStateSelector extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* Render the radio buttons if enabled
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$result = $this->initializeResultArray();
|
||||
|
||||
$fieldName = $this->data['fieldName'];
|
||||
$fieldId = StringUtility::getUniqueId('formengine-localization-state-selector-');
|
||||
$l10nStateFieldName = 'l10n_state';
|
||||
|
||||
$localizationState = State::fromJSON(
|
||||
$this->data['tableName'],
|
||||
$this->data['databaseRow'][$l10nStateFieldName] ?? null
|
||||
);
|
||||
|
||||
if (
|
||||
$localizationState === null
|
||||
|| !isset($this->data['defaultLanguageRow'])
|
||||
|| !isset($this->data['processedTca']['columns'][$fieldName]['config']['behaviour']['allowLanguageSynchronization'])
|
||||
|| !$this->data['processedTca']['columns'][$fieldName]['config']['behaviour']['allowLanguageSynchronization']
|
||||
) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$l10nParentFieldName = $this->data['processedTca']['ctrl']['transOrigPointerField'] ?? null;
|
||||
$l10nSourceFieldName = $this->data['processedTca']['ctrl']['translationSource'] ?? null;
|
||||
|
||||
$sourceLanguageTitle = '';
|
||||
$fieldValueInParentRow = '';
|
||||
$fieldValueInSourceRow = null;
|
||||
if ($l10nParentFieldName && $this->data['databaseRow'][$l10nParentFieldName] > 0) {
|
||||
if ($l10nSourceFieldName && $this->data['databaseRow'][$l10nSourceFieldName] > 0) {
|
||||
$languageField = $this->data['processedTca']['ctrl']['languageField'] ?? null;
|
||||
if ($languageField
|
||||
&& isset($this->data['sourceLanguageRow'][$languageField])
|
||||
&& $this->data['sourceLanguageRow'][$languageField] > 0
|
||||
) {
|
||||
$languageUidOfSourceRow = $this->data['sourceLanguageRow'][$languageField];
|
||||
$sourceLanguageTitle = $this->data['systemLanguageRows'][$languageUidOfSourceRow]['title'] ?? '';
|
||||
$fieldValueInSourceRow = $this->data['sourceLanguageRow'][$fieldName] ?? null;
|
||||
}
|
||||
}
|
||||
$fieldValueInParentRow = (string)$this->data['defaultLanguageRow'][$fieldName];
|
||||
}
|
||||
|
||||
$fieldElementName = 'data[' . htmlspecialchars($this->data['tableName']) . ']'
|
||||
. '[' . htmlspecialchars((string)$this->data['databaseRow']['uid']) . ']'
|
||||
. '[' . htmlspecialchars($l10nStateFieldName) . ']'
|
||||
. '[' . htmlspecialchars($this->data['fieldName']) . ']';
|
||||
|
||||
$html = [];
|
||||
$html[] = '<div class="t3js-l10n-state-container">';
|
||||
$html[] = '<div class="form-label">';
|
||||
$html[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.header');
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div class="form-check">';
|
||||
$html[] = '<input';
|
||||
$html[] = ' id="' . $fieldId . '-custom"';
|
||||
$html[] = ' type="radio"';
|
||||
$html[] = ' name="' . htmlspecialchars($fieldElementName) . '"';
|
||||
$html[] = ' class="form-check-input t3js-l10n-state-custom"';
|
||||
$html[] = ' value="custom"';
|
||||
$html[] = $localizationState->isCustomState($fieldName) ? ' checked="checked"' : '';
|
||||
$html[] = ' data-original-language-value=""';
|
||||
$html[] = '>';
|
||||
$html[] = '<label';
|
||||
$html[] = ' for="' . $fieldId . '-custom"';
|
||||
$html[] = ' class="form-check-label"';
|
||||
$html[] = '>';
|
||||
$html[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.customValue');
|
||||
$html[] = '</label>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div class="form-check">';
|
||||
$html[] = '<input';
|
||||
$html[] = ' id="' . $fieldId . '-parent"';
|
||||
$html[] = ' type="radio"';
|
||||
$html[] = ' name="' . htmlspecialchars($fieldElementName) . '"';
|
||||
$html[] = ' class="form-check-input"';
|
||||
$html[] = ' value="parent"';
|
||||
$html[] = $localizationState->isParentState($fieldName) ? ' checked="checked"' : '';
|
||||
$html[] = ' data-original-language-value="' . htmlspecialchars((string)$fieldValueInParentRow) . '"';
|
||||
$html[] = '>';
|
||||
$html[] = '<label';
|
||||
$html[] = ' for="' . $fieldId . '-parent"';
|
||||
$html[] = ' class="form-check-label"';
|
||||
$html[] = '>';
|
||||
$html[] = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.defaultLanguageValue');
|
||||
$html[] = '</label>';
|
||||
$html[] = '</div>';
|
||||
if ($fieldValueInSourceRow !== null) {
|
||||
$html[] = '<div class="form-check">';
|
||||
$html[] = '<input';
|
||||
$html[] = ' id="' . $fieldId . '-source"';
|
||||
$html[] = ' type="radio"';
|
||||
$html[] = ' name="' . htmlspecialchars($fieldElementName) . '"';
|
||||
$html[] = ' class="form-check-input"';
|
||||
$html[] = ' value="source"';
|
||||
$html[] = $localizationState->isSourceState($fieldName) ? ' checked="checked"' : '';
|
||||
$html[] = ' data-original-language-value="' . htmlspecialchars((string)$fieldValueInSourceRow) . '"';
|
||||
$html[] = '>';
|
||||
$html[] = '<label';
|
||||
$html[] = ' for="' . $fieldId . '-source"';
|
||||
$html[] = ' class="form-check-label"';
|
||||
$html[] = '>';
|
||||
$html[] = sprintf($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_wizards.xlf:localizationStateSelector.sourceLanguageValue'), htmlspecialchars($sourceLanguageTitle));
|
||||
$html[] = '</label>';
|
||||
$html[] = '</div>';
|
||||
}
|
||||
$html[] = '</div>';
|
||||
|
||||
$result['javaScriptModules'][] = JavaScriptModuleInstruction::create(
|
||||
'@typo3/backend/form-engine/field-wizard/localization-state-selector.js'
|
||||
)->instance($fieldElementName);
|
||||
$result['html'] = implode(LF, $html);
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?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\FieldWizard;
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconSize;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Render values of "other" languages. If editing a localized row, this is typically
|
||||
* the content value of the according default record, but it may render field values
|
||||
* of other languages too, depending on configuration.
|
||||
*/
|
||||
class OtherLanguageContent extends AbstractNode
|
||||
{
|
||||
public function __construct(
|
||||
private readonly IconFactory $iconFactory,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Render other language content if enabled.
|
||||
*/
|
||||
public function render(): array
|
||||
{
|
||||
$result = $this->initializeResultArray();
|
||||
|
||||
$fieldName = $this->data['fieldName'];
|
||||
$fieldConfig = $this->data['processedTca']['columns'][$fieldName];
|
||||
$fieldType = $fieldConfig['config']['type'];
|
||||
$l10nDisplay = $this->data['parameterArray']['fieldConf']['l10n_display'] ?? '';
|
||||
$defaultLanguageRow = $this->data['defaultLanguageRow'] ?? null;
|
||||
if (!is_array($defaultLanguageRow)
|
||||
|| GeneralUtility::inList($l10nDisplay, 'hideDiff')
|
||||
|| GeneralUtility::inList($l10nDisplay, 'defaultAsReadonly')
|
||||
|| $fieldType === 'inline'
|
||||
|| $fieldType === 'file'
|
||||
|| $fieldType === 'flex'
|
||||
|| (in_array($fieldType, ['select', 'category', 'group'], true) && isset($fieldConfig['config']['MM']))
|
||||
) {
|
||||
// Early return if there is no default language row or the display is disabled
|
||||
return $result;
|
||||
}
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$html = [];
|
||||
$defaultLanguageValue = BackendUtility::getProcessedValue(
|
||||
$table,
|
||||
$fieldName,
|
||||
$defaultLanguageRow[$fieldName],
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
$defaultLanguageRow['uid'],
|
||||
true,
|
||||
$defaultLanguageRow['pid'],
|
||||
$defaultLanguageRow
|
||||
) ?? '';
|
||||
if ($defaultLanguageValue !== '') {
|
||||
$iconIdentifier = ($this->data['systemLanguageRows'][0]['flagIconIdentifier'] ?? false) ?: 'flags-multiple';
|
||||
$html[] = '<div class="t3-form-original-language">';
|
||||
$html[] = $this->iconFactory->getIcon($iconIdentifier, IconSize::SMALL)->render();
|
||||
$html[] = $this->previewFieldValue($defaultLanguageValue);
|
||||
$html[] = '</div>';
|
||||
}
|
||||
$additionalPreviewLanguages = $this->data['additionalLanguageRows'];
|
||||
foreach ($additionalPreviewLanguages as $previewLanguage) {
|
||||
$defaultLanguageValue = BackendUtility::getProcessedValue(
|
||||
$table,
|
||||
$fieldName,
|
||||
$previewLanguage[$fieldName],
|
||||
0,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
$previewLanguage
|
||||
) ?? '';
|
||||
if ($defaultLanguageValue !== '') {
|
||||
$html[] = '<div class="t3-form-original-language">';
|
||||
$html[] = $this->iconFactory->getIcon($this->data['systemLanguageRows'][$previewLanguage['language_tag']]['flagIconIdentifier'], IconSize::SMALL)->render();
|
||||
$html[] = $this->previewFieldValue($defaultLanguageValue);
|
||||
$html[] = '</div>';
|
||||
}
|
||||
}
|
||||
$result['html'] = implode(LF, $html);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering preview output of a field value which is not shown as a form field but just outputted.
|
||||
*
|
||||
* @param string $value The value to output
|
||||
* @return string HTML formatted output
|
||||
*/
|
||||
protected function previewFieldValue($value)
|
||||
{
|
||||
return nl2br(htmlspecialchars((string)$value));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user