Commit 86f42bc5 authored by Christian Kuhn's avatar Christian Kuhn
Browse files

[TASK] FormEngine: The factory

Creation of container and elements instances in the FormEngine is
hard coded and hard to overwrite or adapt.
The patch extends the existing NodeFactory with resolver code to
find an appropriate class for a given requested type. All FormEngine
internal container and element requests are now routed through
NodeFactory. This allows to loosen the strict dependency between
TCA config "type" to an implementing class by moving the resolving
code into the factory. This is done for SelectElement which is now
split into multiple smaller classes - one for each display type. The
NodeFactory is covered by unit tests since the resolving code will
become more complex and fine grained in the future.
As a side effect the patch resolves a hack in the FormDataTraverser
which no longer calls internal stuff of the select element.
The NodeFactory is prepared to be extended with an API for extensions
to steer and overwrite default implementations. This will be added
with a next patch.

Change-Id: I2253a0fe3240366d0d271a3cd82119ce3dc52012
Resolves: #67006
Releases: master
Reviewed-on: http://review.typo3.org/39517

Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 5569bee6
......@@ -18,6 +18,7 @@ use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\Utility\IconUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Backend\Form\NodeFactory;
/**
* Generation of TCEform elements of the type "input"
......@@ -79,16 +80,17 @@ class InputElement extends AbstractFormElement {
if (in_array('password', $evalList)) {
$itemFormElValue = $itemFormElValue ? '*********' : '';
}
/** @var NoneElement $noneElement */
$noneElement = GeneralUtility::makeInstance(NoneElement::class);
$noneElementOptions = $this->globalOptions;
$noneElementOptions['parameterArray'] = array(
$options = $this->globalOptions;
$options['parameterArray'] = array(
'fieldConf' => array(
'config' => $config,
),
'itemFormElValue' => $itemFormElValue,
);
return $noneElement->setGlobalOptions($noneElementOptions)->render();
$options['type'] = 'none';
/** @var NodeFactory $nodeFactory */
$nodeFactory = $this->globalOptions['nodeFactory'];
return $nodeFactory->create($options)->render();
}
if (in_array('datetime', $evalList, TRUE)
......
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Backend\Utility\IconUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
/**
* Creates a widget with check box elements.
*
* This is rendered for config type=select, renderMode=checkbox, maxitems > 1
*/
class SelectCheckBoxElement extends AbstractFormElement {
/**
* @var array Result array given returned by render() - This property is a helper until class is properly refactored
*/
protected $resultArray = array();
/**
* Render check boxes
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render() {
$table = $this->globalOptions['table'];
$field = $this->globalOptions['fieldName'];
$row = $this->globalOptions['databaseRow'];
$parameterArray = $this->globalOptions['parameterArray'];
// Field configuration from TCA:
$config = $parameterArray['fieldConf']['config'];
$disabled = '';
if ($this->isGlobalReadonly() || $config['readOnly']) {
$disabled = ' disabled="disabled"';
}
$this->resultArray = $this->initializeResultArray();
// "Extra" configuration; Returns configuration for the field based on settings found in the "types" fieldlist.
$specConf = BackendUtility::getSpecConfParts($parameterArray['extra'], $parameterArray['fieldConf']['defaultExtras']);
$selItems = FormEngineUtility::getSelectItems($table, $field, $row, $parameterArray);
// Creating the label for the "No Matching Value" entry.
$noMatchingLabel = isset($parameterArray['fieldTSConfig']['noMatchingValue_label'])
? $this->getLanguageService()->sL($parameterArray['fieldTSConfig']['noMatchingValue_label'])
: '[ ' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.noMatchingValue') . ' ]';
$html = $this->getSingleField_typeSelect_checkbox($table, $field, $row, $parameterArray, $config, $selItems, $noMatchingLabel);
// Wizards:
if (!$disabled) {
$altItem = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
$html = $this->renderWizards(array($html, $altItem), $config['wizards'], $table, $row, $field, $parameterArray, $parameterArray['itemFormElName'], $specConf);
}
$this->resultArray['html'] = $html;
return $this->resultArray;
}
/**
* Creates a checkbox list (renderMode = "checkbox")
*
* @param string $table See getSingleField_typeSelect()
* @param string $field See getSingleField_typeSelect()
* @param array $row See getSingleField_typeSelect()
* @param array $parameterArray See getSingleField_typeSelect()
* @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience)
* @param array $selItems Items available for selection
* @param string $noMatchingLabel Label for no-matching-value
* @return string The HTML code for the item
*/
protected function getSingleField_typeSelect_checkbox($table, $field, $row, $parameterArray, $config, $selItems, $noMatchingLabel) {
if (empty($selItems)) {
return '';
}
// Get values in an array (and make unique, which is fine because there can be no duplicates anyway):
$itemArray = array_flip(FormEngineUtility::extractValuesOnlyFromValueLabelList($parameterArray['itemFormElValue']));
$output = '';
// Disabled
$disabled = 0;
if ($this->isGlobalReadonly() || $config['readOnly']) {
$disabled = 1;
}
// Traverse the Array of selector box items:
$groups = array();
$currentGroup = 0;
$c = 0;
$sOnChange = '';
if (!$disabled) {
$sOnChange = implode('', $parameterArray['fieldChangeFunc']);
// Used to accumulate the JS needed to restore the original selection.
foreach ($selItems as $p) {
// Non-selectable element:
if ($p[1] === '--div--') {
$selIcon = '';
if (isset($p[2]) && $p[2] != 'empty-emtpy') {
$selIcon = FormEngineUtility::getIconHtml($p[2]);
}
$currentGroup++;
$groups[$currentGroup]['header'] = array(
'icon' => $selIcon,
'title' => htmlspecialchars($p[0])
);
} else {
// Check if some help text is available
// Since TYPO3 4.5 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 itemProcFunc)
$hasHelp = FALSE;
$help = '';
$helpArray = array();
if (is_array($p[3]) && count($p[3]) > 0 || !empty($p[3])) {
$hasHelp = TRUE;
if (is_array($p[3])) {
$helpArray = $p[3];
} else {
$helpArray['description'] = $p[3];
}
}
if ($hasHelp) {
$help = BackendUtility::wrapInHelp('', '', '', $helpArray);
}
// Selected or not by default:
$checked = 0;
if (isset($itemArray[$p[1]])) {
$checked = 1;
unset($itemArray[$p[1]]);
}
// Build item array
$groups[$currentGroup]['items'][] = array(
'id' => str_replace('.', '', uniqid('select_checkbox_row_', TRUE)),
'name' => $parameterArray['itemFormElName'] . '[' . $c . ']',
'value' => $p[1],
'checked' => $checked,
'disabled' => $disabled,
'class' => '',
'icon' => (!empty($p[2]) ? FormEngineUtility::getIconHtml($p[2]) : IconUtility::getSpriteIcon('empty-empty')),
'title' => htmlspecialchars($p[0], ENT_COMPAT, 'UTF-8', FALSE),
'help' => $help
);
$c++;
}
}
}
// Remaining values (invalid):
if (count($itemArray) && !$parameterArray['fieldTSConfig']['disableNoMatchingValueElement'] && !$config['disableNoMatchingValueElement']) {
$currentGroup++;
foreach ($itemArray as $theNoMatchValue => $temp) {
// Build item array
$groups[$currentGroup]['items'][] = array(
'id' => str_replace('.', '', uniqid('select_checkbox_row_', TRUE)),
'name' => $parameterArray['itemFormElName'] . '[' . $c . ']',
'value' => $theNoMatchValue,
'checked' => 1,
'disabled' => $disabled,
'class' => 'danger',
'icon' => '',
'title' => htmlspecialchars(@sprintf($noMatchingLabel, $theNoMatchValue), ENT_COMPAT, 'UTF-8', FALSE),
'help' => ''
);
$c++;
}
}
// Add an empty hidden field which will send a blank value if all items are unselected.
$output .= '<input type="hidden" class="select-checkbox" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '" value="" />';
// Building the checkboxes
foreach($groups as $groupKey => $group){
$groupId = htmlspecialchars($parameterArray['itemFormElID']) . '-group-' . $groupKey;
$output .= '<div class="panel panel-default">';
if(is_array($group['header'])){
$output .= '
<div class="panel-heading">
<a data-toggle="collapse" href="#' . $groupId . '" aria-expanded="true" aria-controls="' . $groupId . '">
' . $group['header']['icon'] . '
' . $group['header']['title'] . '
</a>
</div>
';
}
if(is_array($group['items']) && count($group['items']) >= 1){
$tableRows = '';
$checkGroup = array();
$uncheckGroup = array();
$resetGroup = array();
// Render rows
foreach($group['items'] as $item){
$tableRows .= '
<tr class="' . $item['class'] . '">
<td class="col-checkbox">
<input type="checkbox"
id="' . $item['id'] . '"
name="' . htmlspecialchars($item['name']) . '"
value="' . htmlspecialchars($item['value']) . '"
onclick="' . htmlspecialchars($sOnChange) . '"
' . ($item['checked'] ? ' checked=checked' : '') . '
' . ($item['disabled'] ? ' disabled=disabled' : '') . '
' . $parameterArray['onFocus'] . ' />
</td>
<td class="col-icon">
<label class="label-block" for="' . $item['id'] . '">' . $item['icon'] . '</label>
</td>
<td class="col-title">
<label class="label-block" for="' . $item['id'] . '">' . $item['title'] . '</label>
</td>
<td>' . $item['help'] . '</td>
</tr>
';
$checkGroup[] = 'document.editform[' . GeneralUtility::quoteJSvalue($item['name']) . '].checked=1;';
$uncheckGroup[] = 'document.editform[' . GeneralUtility::quoteJSvalue($item['name']) . '].checked=0;';
$resetGroup[] = 'document.editform[' . GeneralUtility::quoteJSvalue($item['name']) . '].checked='.$item['checked'] . ';';
}
// Build toggle group checkbox
$toggleGroupCheckbox = '';
if(count($resetGroup)){
$toggleGroupCheckbox = '
<input type="checkbox" class="checkbox" onclick="if (checked) {' . htmlspecialchars(implode('', $checkGroup) . '} else {' . implode('', $uncheckGroup)) . '}">
';
}
// Build reset group button
$resetGroupBtn = '';
if(count($resetGroup)){
$resetGroupBtn = '
<a href="#" class="btn btn-default" onclick="' . implode('', $resetGroup) . ' return false;' . '">
' . IconUtility::getSpriteIcon('actions-edit-undo', array('title' => htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.revertSelection')))) . '
' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.revertSelection') . '
</a>
';
}
$output .= '
<div id="' . $groupId . '" class="panel-collapse collapse in" role="tabpanel">
<div class="table-fit">
<table class="table table-transparent table-hover">
<thead>
<tr>
<th class="col-checkbox">' . $toggleGroupCheckbox . '</th>
<th class="col-icon"></th>
<th class="text-right" colspan="2">' . $resetGroupBtn . '</th>
</tr>
</thead>
<tbody>' . $tableRows . '</tbody>
</table>
</div>
</div>
';
}
$output .= '</div>';
}
return $output;
}
}
<?php
namespace TYPO3\CMS\Backend\Form\Element;
/*
* 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!
*/
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\MathUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
/**
* Render a widget with two boxes side by side.
*
* This is rendered for config type=select, maxitems > 1, no other renderMode set
*/
class SelectMultipleSideBySideElement extends AbstractFormElement {
/**
* @var array Result array given returned by render() - This property is a helper until class is properly refactored
*/
protected $resultArray = array();
/**
* Render side by side element.
*
* @return array As defined in initializeResultArray() of AbstractNode
*/
public function render() {
$table = $this->globalOptions['table'];
$field = $this->globalOptions['fieldName'];
$row = $this->globalOptions['databaseRow'];
$parameterArray = $this->globalOptions['parameterArray'];
// Field configuration from TCA:
$config = $parameterArray['fieldConf']['config'];
$disabled = '';
if ($this->isGlobalReadonly() || $config['readOnly']) {
$disabled = ' disabled="disabled"';
}
$this->resultArray = $this->initializeResultArray();
// "Extra" configuration; Returns configuration for the field based on settings found in the "types" fieldlist.
$specConf = BackendUtility::getSpecConfParts($parameterArray['extra'], $parameterArray['fieldConf']['defaultExtras']);
$selItems = FormEngineUtility::getSelectItems($table, $field, $row, $parameterArray);
// Creating the label for the "No Matching Value" entry.
$noMatchingLabel = isset($parameterArray['fieldTSConfig']['noMatchingValue_label'])
? $this->getLanguageService()->sL($parameterArray['fieldTSConfig']['noMatchingValue_label'])
: '[ ' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.noMatchingValue') . ' ]';
$html = $this->getSingleField_typeSelect_multiple($table, $field, $row, $parameterArray, $config, $selItems, $noMatchingLabel);
// Wizards:
if (!$disabled) {
$altItem = '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '" value="' . htmlspecialchars($parameterArray['itemFormElValue']) . '" />';
$html = $this->renderWizards(array($html, $altItem), $config['wizards'], $table, $row, $field, $parameterArray, $parameterArray['itemFormElName'], $specConf);
}
$this->resultArray['html'] = $html;
return $this->resultArray;
}
/**
* Creates a multiple-selector box (two boxes, side-by-side)
*
* @param string $table See getSingleField_typeSelect()
* @param string $field See getSingleField_typeSelect()
* @param array $row See getSingleField_typeSelect()
* @param array $parameterArray See getSingleField_typeSelect()
* @param array $config (Redundant) content of $PA['fieldConf']['config'] (for convenience)
* @param array $selItems Items available for selection
* @param string $noMatchingLabel Label for no-matching-value
* @return string The HTML code for the item
*/
protected function getSingleField_typeSelect_multiple($table, $field, $row, $parameterArray, $config, $selItems, $noMatchingLabel) {
$languageService = $this->getLanguageService();
$item = '';
$disabled = '';
if ($this->isGlobalReadonly() || $config['readOnly']) {
$disabled = ' disabled="disabled"';
}
// Setting this hidden field (as a flag that JavaScript can read out)
if (!$disabled) {
$item .= '<input type="hidden" name="' . $parameterArray['itemFormElName'] . '_mul" value="' . ($config['multiple'] ? 1 : 0) . '" />';
}
// Set max and min items:
$maxitems = MathUtility::forceIntegerInRange($config['maxitems'], 0);
if (!$maxitems) {
$maxitems = 100000;
}
$minitems = MathUtility::forceIntegerInRange($config['minitems'], 0);
// Register the required number of elements:
$this->resultArray['requiredElements'][$parameterArray['itemFormElName']] = array(
$minitems,
$maxitems,
'imgName' => $table . '_' . $row['uid'] . '_' . $field
);
$tabAndInlineStack = $this->globalOptions['tabAndInlineStack'];
if (!empty($tabAndInlineStack) && preg_match('/^(.+\\])\\[(\\w+)\\]$/', $parameterArray['itemFormElName'], $match)) {
array_shift($match);
$this->resultArray['requiredNested'][$parameterArray['itemFormElName']] = array(
'parts' => $match,
'level' => $tabAndInlineStack,
);
}
// Get "removeItems":
$removeItems = GeneralUtility::trimExplode(',', $parameterArray['fieldTSConfig']['removeItems'], TRUE);
// Get the array with selected items:
$itemArray = GeneralUtility::trimExplode(',', $parameterArray['itemFormElValue'], TRUE);
// Possibly filter some items:
$itemArray = ArrayUtility::keepItemsInArray(
$itemArray,
$parameterArray['fieldTSConfig']['keepItems'],
function ($value) {
$parts = explode('|', $value, 2);
return rawurldecode($parts[0]);
}
);
// Perform modification of the selected items array:
foreach ($itemArray as $tk => $tv) {
$tvP = explode('|', $tv, 2);
$evalValue = $tvP[0];
$isRemoved = in_array($evalValue, $removeItems)
|| $config['type'] == 'select' && $config['authMode']
&& !$this->getBackendUserAuthentication()->checkAuthMode($table, $field, $evalValue, $config['authMode']);
if ($isRemoved && !$parameterArray['fieldTSConfig']['disableNoMatchingValueElement'] && !$config['disableNoMatchingValueElement']) {
$tvP[1] = rawurlencode(@sprintf($noMatchingLabel, $evalValue));
} else {
if (isset($parameterArray['fieldTSConfig']['altLabels.'][$evalValue])) {
$tvP[1] = rawurlencode($languageService->sL($parameterArray['fieldTSConfig']['altLabels.'][$evalValue]));
}
if (isset($parameterArray['fieldTSConfig']['altIcons.'][$evalValue])) {
$tvP[2] = $parameterArray['fieldTSConfig']['altIcons.'][$evalValue];
}
}
if ($tvP[1] == '') {
// Case: flexform, default values supplied, no label provided (bug #9795)
foreach ($selItems as $selItem) {
if ($selItem[1] == $tvP[0]) {
$tvP[1] = html_entity_decode($selItem[0]);
break;
}
}
}
$itemArray[$tk] = implode('|', $tvP);
}
$itemsToSelect = '';
$filterTextfield = '';
$filterSelectbox = '';
$size = 0;
if (!$disabled) {
// Create option tags:
$opt = array();
$styleAttrValue = '';
foreach ($selItems as $p) {
if ($config['iconsInOptionTags']) {
$styleAttrValue = FormEngineUtility::optionTagStyle($p[2]);
}
$opt[] = '<option value="' . htmlspecialchars($p[1]) . '"'
. ($styleAttrValue ? ' style="' . htmlspecialchars($styleAttrValue) . '"' : '')
. ' title="' . $p[0] . '">' . $p[0] . '</option>';
}
// Put together the selector box:
$selector_itemListStyle = isset($config['itemListStyle'])
? ' style="' . htmlspecialchars($config['itemListStyle']) . '"'
: '';
$size = (int)$config['size'];
$size = $config['autoSizeMax']
? MathUtility::forceIntegerInRange(count($itemArray) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax'])
: $size;
$sOnChange = implode('', $parameterArray['fieldChangeFunc']);
$multiSelectId = str_replace('.', '', uniqid('tceforms-multiselect-', TRUE));
$itemsToSelect = '
<select data-relatedfieldname="' . htmlspecialchars($parameterArray['itemFormElName']) . '" data-exclusivevalues="'
. htmlspecialchars($config['exclusiveKeys']) . '" id="' . $multiSelectId . '" name="' . htmlspecialchars($parameterArray['itemFormElName']) . '_sel" '
. ' class="form-control t3js-formengine-select-itemstoselect" '
. ($size ? ' size="' . $size . '"' : '') . ' onchange="' . htmlspecialchars($sOnChange) . '"'
. $parameterArray['onFocus'] . $selector_itemListStyle . '>
' . implode('
', $opt) . '
</select>';
// enable filter functionality via a text field
if ($config['enableMultiSelectFilterTextfield']) {
$filterTextfield = '
<span class="input-group input-group-sm">
<span class="input-group-addon">
<span class="fa fa-filter"></span>
</span>
<input class="t3js-formengine-multiselect-filter-textfield form-control" value="" />
</span>';
}
// enable filter functionality via a select
if (isset($config['multiSelectFilterItems']) && is_array($config['multiSelectFilterItems']) && count($config['multiSelectFilterItems']) > 1) {
$filterDropDownOptions = array();
foreach ($config['multiSelectFilterItems'] as $optionElement) {
$optionValue = $languageService->sL(isset($optionElement[1]) && $optionElement[1] != '' ? $optionElement[1]
: $optionElement[0]);
$filterDropDownOptions[] = '<option value="' . htmlspecialchars($languageService->sL($optionElement[0])) . '">'
. htmlspecialchars($optionValue) . '</option>';
}
$filterSelectbox = '<select class="form-control input-sm t3js-formengine-multiselect-filter-dropdown">
' . implode('
', $filterDropDownOptions) . '
</select>';
}
}
if (!empty(trim($filterSelectbox)) && !empty(trim($filterTextfield))) {
$filterSelectbox = '<div class="form-multigroup-item form-multigroup-element">' . $filterSelectbox . '</div>';
$filterTextfield = '<div class="form-multigroup-item form-multigroup-element">' . $filterTextfield . '</div>';
$selectBoxFilterContents = '<div class="t3js-formengine-multiselect-filter-container form-multigroup-wrap">' . $filterSelectbox . $filterTextfield . '</div>';
} else {
$selectBoxFilterContents = trim($filterSelectbox . ' ' . $filterTextfield);
}
// Pass to "dbFileIcons" function:
$params = array(
'size' => $size,
'autoSizeMax' => MathUtility::forceIntegerInRange($config['autoSizeMax'], 0),
'style' => isset($config['selectedListStyle'])
? ' style="' . htmlspecialchars($config['selectedListStyle']) . '"'
: '',
'dontShowMoveIcons' => $maxitems <= 1,
'maxitems' => $maxitems,
'info' => '',
'headers' => array(
'selector' => $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.selected'),
'items' => $languageService->sL('LLL:EXT:lang/locallang_core.xlf:labels.items'),
'selectorbox' => $selectBoxFilterContents,
),
'noBrowser' => 1,
'rightbox' => $itemsToSelect,
'readOnly' => $disabled
);
$item .= $this->dbFileIcons($parameterArray['itemFormElName'], '', '', $itemArray, '', $params, $parameterArray['onFocus']);
return $item;
}
/**
* @return BackendUserAuthentication
*/
protected function getBackendUserAuthentication() {