diff --git a/typo3/sysext/backend/Classes/Controller/SelectTreeController.php b/typo3/sysext/backend/Classes/Controller/SelectTreeController.php index a0f6423a6b2b68ba22809a6a9768f5ebe4bfa5d5..eee0d931519ffe0d4421e0ca04953a0614534855 100644 --- a/typo3/sysext/backend/Classes/Controller/SelectTreeController.php +++ b/typo3/sysext/backend/Classes/Controller/SelectTreeController.php @@ -105,12 +105,12 @@ class SelectTreeController if ($formData['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') { $treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds'] - ['sheets'][$flexFormPath[3]]['ROOT']['el'][$flexFormPath[5]]['config']['treeData']; + ['sheets'][$flexFormPath[3]]['ROOT']['el'][$flexFormPath[5]]['config']['items']; } else { - $treeData = $formData['processedTca']['columns'][$fieldName]['config']['treeData']; + $treeData = $formData['processedTca']['columns'][$fieldName]['config']['items']; } - $json = json_encode($treeData['items']); + $json = json_encode($treeData); $response->getBody()->write($json); return $response; } diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php index 58f001feb7c247a852958fa9d9b97a751bc64b16..f388e6f18401ae19f6badcc56c4a634b6044b732 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/AbstractItemProvider.php @@ -1358,8 +1358,9 @@ abstract class AbstractItemProvider * @param array $itemArray All item records for the select field * @param array $dynamicItemArray Item records from dynamic sources * @return array + * @todo: Check method usage, it's probably bogus in select context and was removed from select tree already. */ - public function getStaticValues($itemArray, $dynamicItemArray) + protected function getStaticValues($itemArray, $dynamicItemArray) { $staticValues = []; foreach ($itemArray as $key => $item) { diff --git a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php index 92fed04939be7fd3cee7e68f5a82ba10e153250a..ebd1b6321e6101aebcaadf7335f35cd4755d557f 100644 --- a/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php +++ b/typo3/sysext/backend/Classes/Form/FormDataProvider/TcaSelectTreeItems.php @@ -21,12 +21,20 @@ use TYPO3\CMS\Core\Tree\TableConfiguration\TreeDataProviderFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; /** - * Resolve select items, set processed item list in processedTca, sanitize and resolve database field + * Data provider for type=select + renderType=selectTree fields. + * + * Used in combination with SelectTreeElement to create the base HTML for trees, + * does a little bit of sanitation and preparation then. + * + * Used in combination with SelectTreeController to fetch the final tree list, this is + * triggered if $result['selectTreeCompileItems'] is set to true. This way the tree item + * calculation is only triggered if needed in this ajax context. Writes the prepared + * item array to ['config']['items'] in this case. */ class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProviderInterface { /** - * Resolve select items + * Sanitize config options and resolve select items if requested. * * @param array $result * @return array @@ -50,7 +58,7 @@ class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProvide // A couple of tree specific config parameters can be overwritten via page TS. // Pick those that influence the data fetching and write them into the config - // given to the tree data provider + // given to the tree data provider. This is additionally used in SelectTreeElement, so always do that. if (isset($result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.'])) { $pageTsConfig = $result['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.']['config.']['treeConfig.']; // If rootUid is set in pageTsConfig, use it @@ -69,42 +77,93 @@ class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProvide } if ($result['selectTreeCompileItems']) { - $fieldConfig['config']['items'] = $this->sanitizeItemArray($fieldConfig['config']['items'], $table, $fieldName); - - $pageTsConfigAddItems = $this->addItemsFromPageTsConfig($result, $fieldName, []); - $fieldConfig['config']['items'] = $this->addItemsFromSpecial($result, $fieldName, $fieldConfig['config']['items']); - $fieldConfig['config']['items'] = $this->addItemsFromFolder($result, $fieldName, $fieldConfig['config']['items']); - $staticItems = $fieldConfig['config']['items'] + $pageTsConfigAddItems; - - $fieldConfig['config']['items'] = $this->addItemsFromForeignTable($result, $fieldName, $fieldConfig['config']['items']); - $dynamicItems = array_diff_key($fieldConfig['config']['items'], $staticItems); - - $fieldConfig['config']['items'] = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']); - $fieldConfig['config']['items'] = $pageTsConfigAddItems + $fieldConfig['config']['items']; - $fieldConfig['config']['items'] = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $fieldConfig['config']['items']); - - $fieldConfig['config']['items'] = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $fieldConfig['config']['items']); - $fieldConfig['config']['items'] = $this->removeItemsByUserAuthMode($result, $fieldName, $fieldConfig['config']['items']); - $fieldConfig['config']['items'] = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $fieldConfig['config']['items']); - - // Resolve "itemsProcFunc" - if (!empty($fieldConfig['config']['itemsProcFunc'])) { - $fieldConfig['config']['items'] = $this->resolveItemProcessorFunction($result, $fieldName, $fieldConfig['config']['items']); - // itemsProcFunc must not be used anymore - unset($fieldConfig['config']['itemsProcFunc']); - } - - // Translate labels - $fieldConfig['config']['items'] = $this->translateLabels($result, $fieldConfig['config']['items'], $table, $fieldName); - - $staticValues = $this->getStaticValues($fieldConfig['config']['items'], $dynamicItems); + // Prepare the list of currently selected nodes using RelationHandler $result['databaseRow'][$fieldName] = $this->processDatabaseFieldValue($result['databaseRow'], $fieldName); - $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, $staticValues); - - // Keys may contain table names, so a numeric array is created - $fieldConfig['config']['items'] = array_values($fieldConfig['config']['items']); + $result['databaseRow'][$fieldName] = $this->processSelectFieldValue($result, $fieldName, []); + + $finalItems = []; + + // Prepare the list of "static" items if there are any. + // "static" and "dynamic" is separated since the tree code only copes with "real" existing foreign nodes, + // so this "static" stuff allows defining tree items that don't really exist in the tree. + $itemsFromTca = $this->sanitizeItemArray($fieldConfig['config']['items'], $table, $fieldName); + // List of additional items defined by page ts config "addItems" + $itemsFromPageTsConfig = $this->addItemsFromPageTsConfig($result, $fieldName, []); + if (!empty($itemsFromTca) || !empty($itemsFromPageTsConfig)) { + // First apply "keepItems" to $itemsFromTca, this will restrict the tca item list to only + // those items that are defined in page ts "keepItems" if given + $itemsFromTca = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $itemsFromTca); + // Then, merge the items from page ts "addItems" into item list, since "addItems" should + // add additional items even if they are not in the "keepItems" list + $staticItems = array_merge($itemsFromTca, $itemsFromPageTsConfig); + // Now apply page ts config "removeItems", so this is *after* addItems, so "removeItems" could + // possibly remove items again that were added via "addItems" + $staticItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $staticItems); + // Now, apply user and access right restrictions to this item list + $staticItems = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $staticItems); + $staticItems = $this->removeItemsByUserAuthMode($result, $fieldName, $staticItems); + $staticItems = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $staticItems); + // Call itemsProcFunc if given. Note this function does *not* see the "dynamic" list of items + if (!empty($fieldConfig['config']['itemsProcFunc'])) { + $staticItems = $this->resolveItemProcessorFunction($result, $fieldName, $staticItems); + // itemsProcFunc must not be used anymore + unset($fieldConfig['config']['itemsProcFunc']); + } + // And translate any labels from the static list + $staticItems = $this->translateLabels($result, $staticItems, $table, $fieldName); + // Now compile the target items using the same array structure as the "dynamic" list below + foreach ($staticItems as $item) { + if ($item[1] === '--div--') { + // Skip divs that may occur here for whatever reason + continue; + } + $finalItems[] = [ + 'identifier' => $item[1], + 'name' => $item[0], + 'icon' => $item[2] ?? '', + 'iconOverlay' => '', + 'depth' => 0, + 'hasChildren' => false, + 'selectable' => true, + 'checked' => in_array($item[1], $result['databaseRow'][$fieldName]), + ]; + } + } - $fieldConfig['config']['treeData'] = $this->renderTree($result, $fieldConfig, $fieldName, $staticItems); + // Fetch the list of all possible "related" items (yuk!) and apply a similar processing as with the "static" list + $dynamicItems = $this->addItemsFromForeignTable($result, $fieldName, []); + $dynamicItems = $this->removeItemsByKeepItemsPageTsConfig($result, $fieldName, $dynamicItems); + $dynamicItems = $this->removeItemsByRemoveItemsPageTsConfig($result, $fieldName, $dynamicItems); + $dynamicItems = $this->removeItemsByUserLanguageFieldRestriction($result, $fieldName, $dynamicItems); + $dynamicItems = $this->removeItemsByUserAuthMode($result, $fieldName, $dynamicItems); + $dynamicItems = $this->removeItemsByDoktypeUserRestriction($result, $fieldName, $dynamicItems); + // Funnily, the only data needed for the tree code are the uids of the possible records (yuk!) - get them + $uidListOfAllDynamicItems = []; + foreach ($dynamicItems as $item) { + if ((int)$item[1] > 0) { + $uidListOfAllDynamicItems[] = (int)$item[1]; + } + } + // Now kick in this tree stuff + $treeDataProvider = TreeDataProviderFactory::getDataProvider( + $fieldConfig['config'], + $table, + $fieldName, + $result['databaseRow'] + ); + $treeDataProvider->setSelectedList(implode(',', $result['databaseRow'][$fieldName])); + // Basically the tree foo fetches all tree nodes again (aaargs), then verifies if + // a given rows uid is within this "list of allowed uids". It then creates an object + // tree representing the nested tree, just to collapse all that to a flat array again. Yay ... + $treeDataProvider->setItemWhiteList($uidListOfAllDynamicItems); + $treeDataProvider->initializeTreeData(); + $treeRenderer = GeneralUtility::makeInstance(ExtJsArrayTreeRenderer::class); + $tree = GeneralUtility::makeInstance(TableConfigurationTree::class); + $tree->setDataProvider($treeDataProvider); + $tree->setNodeRenderer($treeRenderer); + + // Merge tree nodes after calculated nodes from static items + $fieldConfig['config']['items'] = array_merge($finalItems, $tree->render()); } $result['processedTca']['columns'][$fieldName] = $fieldConfig; @@ -113,81 +172,6 @@ class TcaSelectTreeItems extends AbstractItemProvider implements FormDataProvide return $result; } - /** - * Renders the Ext JS tree. - * - * @param array $result The current result array. - * @param array $fieldConfig The configuration of the current field. - * @param string $fieldName The name of the current field. - * @param array $staticItems The static items from the field config. - * @return array The tree data configuration - */ - protected function renderTree(array $result, array $fieldConfig, $fieldName, array $staticItems) - { - $allowedUids = []; - foreach ($fieldConfig['config']['items'] as $item) { - if ((int)$item[1] > 0) { - $allowedUids[] = $item[1]; - } - } - - $treeDataProvider = TreeDataProviderFactory::getDataProvider( - $fieldConfig['config'], - $result['tableName'], - $fieldName, - $result['databaseRow'] - ); - $treeDataProvider->setSelectedList(is_array($result['databaseRow'][$fieldName]) ? implode(',', $result['databaseRow'][$fieldName]) : $result['databaseRow'][$fieldName]); - $treeDataProvider->setItemWhiteList($allowedUids); - $treeDataProvider->initializeTreeData(); - - /** @var ExtJsArrayTreeRenderer $treeRenderer */ - $treeRenderer = GeneralUtility::makeInstance(ExtJsArrayTreeRenderer::class); - - /** @var TableConfigurationTree $tree */ - $tree = GeneralUtility::makeInstance(TableConfigurationTree::class); - $tree->setDataProvider($treeDataProvider); - $tree->setNodeRenderer($treeRenderer); - - $treeItems = $this->prepareAdditionalItems($staticItems, $result['databaseRow'][$fieldName]); - $treeItems[] = $tree->render(); - - $treeConfig = [ - 'items' => $treeItems, - ]; - - return $treeConfig; - } - - /** - * Prepare the additional items that get prepended to the tree as leaves - * - * @param array $itemArray - * @param array $selectedNodes - * @return array - */ - protected function prepareAdditionalItems(array $itemArray, array $selectedNodes) - { - $additionalItems = []; - - foreach ($itemArray as $item) { - if ($item[1] === '--div--') { - continue; - } - - $additionalItems[] = [ - 'uid' => $item[1], - 'text' => $item[0], - 'selectable' => true, - 'leaf' => true, - 'checked' => in_array($item[1], $selectedNodes), - 'icon' => $item[2] - ]; - } - - return $additionalItems; - } - /** * Determines whether the current field is a valid target for this DataProvider * diff --git a/typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php b/typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php index 4ff6365a8e3da3a9a47a0e7f49b8bb42fb63b661..12bd8622a239e031b2cdbddb2f05dc4c211f716e 100644 --- a/typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php +++ b/typo3/sysext/backend/Classes/Tree/Renderer/ExtJsJsonTreeRenderer.php @@ -13,7 +13,9 @@ namespace TYPO3\CMS\Backend\Tree\Renderer; * * The TYPO3 project - inspiring people to share! */ +use TYPO3\CMS\Backend\Tree\TreeNodeCollection; use TYPO3\CMS\Core\Imaging\IconProvider\SvgIconProvider; +use TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeNode; /** * Renderer for unordered lists @@ -36,11 +38,14 @@ class ExtJsJsonTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTre */ public function renderNode(\TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node, $recursive = true) { - $nodeArray = $this->getNodeArray($node); + $nodeArray = []; + $nodeArray[] = $this->getNodeArray($node); if ($recursive && $node->hasChildNodes()) { $this->recursionLevel++; $children = $this->renderNodeCollection($node->getChildNodes()); - $nodeArray['children'] = $children; + foreach ($children as $child) { + $nodeArray[] = $child; + } $this->recursionLevel--; } return $nodeArray; @@ -49,7 +54,7 @@ class ExtJsJsonTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTre /** * Get node array * - * @param \TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node + * @param \TYPO3\CMS\Backend\Tree\TreeRepresentationNode|DatabaseTreeNode $node * @return array */ protected function getNodeArray(\TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node) @@ -64,27 +69,29 @@ class ExtJsJsonTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTre $iconMarkup = $node->getIcon(); } $nodeArray = [ - 'iconTag' => $iconMarkup, - 'text' => htmlspecialchars($node->getLabel()), - 'leaf' => !$node->hasChildNodes(), - 'id' => htmlspecialchars($node->getId()), - 'uid' => htmlspecialchars($node->getId()), - - //svgtree - 'icon' => $iconMarkup, - 'overlayIcon' => $overlayIconMarkup, 'identifier' => htmlspecialchars($node->getId()), - //no need for htmlspecialhars here as d3 is using 'textContent' property of the HTML DOM node + // No need for htmlspecialchars() here as d3 is using 'textContent' property of the HTML DOM node 'name' => $node->getLabel(), + 'icon' => $iconMarkup, + 'overlayIcon' => $overlayIconMarkup, + 'depth' => $this->recursionLevel, + 'hasChildren' => (bool)$node->hasChildNodes(), + 'selectable' => true, ]; - + if ($node instanceof DatabaseTreeNode) { + $nodeArray['checked'] = (bool)$node->getSelected(); + if (!$node->getSelectable()) { + $nodeArray['checked'] = false; + $nodeArray['selectable'] = false; + } + } return $nodeArray; } /** * Renders a node collection recursive or just a single instance * - * @param \TYPO3\CMS\Backend\Tree\TreeNodeCollection $node + * @param \TYPO3\CMS\Backend\Tree\AbstractTree $tree * @param bool $recursive * @return string */ @@ -98,14 +105,24 @@ class ExtJsJsonTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\AbstractTre /** * Renders an tree recursive or just a single instance * - * @param \TYPO3\CMS\Backend\Tree\AbstractTree $node + * @param TreeNodeCollection $collection * @param bool $recursive * @return array */ - public function renderNodeCollection(\TYPO3\CMS\Backend\Tree\TreeNodeCollection $collection, $recursive = true) + public function renderNodeCollection(TreeNodeCollection $collection, $recursive = true) { + $treeItems = []; foreach ($collection as $node) { - $treeItems[] = $this->renderNode($node, $recursive); + $allNodes = $this->renderNode($node, $recursive); + if ($allNodes[0]) { + $treeItems[] = $allNodes[0]; + } + $nodeCount = count($allNodes); + if ($nodeCount > 1) { + for ($i = 1; $i < $nodeCount; $i++) { + $treeItems[] = $allNodes[$i]; + } + } } return $treeItems; } diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js index a20c7221faf1fc0a8de412deae899d49782a5277..e908dbc2f31dcae563868eecc3056d4224ac2cdc 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SelectTree.js @@ -59,7 +59,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg nodeSelection .selectAll('.tree-check use') .attr('visibility', function (node) { - var checked = Boolean(node.data.checked); + var checked = Boolean(node.checked); if (d3.select(this).classed('icon-checked') && checked) { return 'visible'; } else if (d3.select(this).classed('icon-indeterminate') && node.indeterminate && !checked) { @@ -85,7 +85,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg //this can be simplified to single "use" element with changing href on click when we drop IE11 on WIN7 support var g = nodeSelection.filter(function (node) { //do not render checkbox if node is not selectable - return me.isNodeSelectable(node) || Boolean(node.data.checked); + return me.isNodeSelectable(node) || Boolean(node.checked); }) .append('g') .attr('class', 'tree-check') @@ -124,7 +124,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg } return node.children.some(function (child) { - if (child.data.checked || child.indeterminate) { + if (child.checked || child.indeterminate) { return true; } }); @@ -138,8 +138,9 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg SelectTree.prototype.updateAncestorsIndetermineState = function (node) { var me = this; //foreach ancestor except node itself - node.ancestors().slice(1).forEach(function (n) { - n.indeterminate = (node.data.checked || node.indeterminate) ? true : me.hasCheckedOrIndeterminateChildren(n); + node.parents.forEach(function (index) { + var n = me.nodes[index]; + n.indeterminate = (node.checked || node.indeterminate) ? true : me.hasCheckedOrIndeterminateChildren(n); }); }; @@ -149,12 +150,12 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg * It's done once after loading data. Later indeterminate state is updated just for the subset of nodes */ SelectTree.prototype.loadDataAfter = function () { - this.rootNode.each(function (node) { + this.nodes.forEach(function (node) { node.indeterminate = false; }); - this.calculateIndeterminate(this.rootNode); + this.calculateIndeterminate(this.nodes); // Initialise "value" attribute of input field after load and revalidate form engine fields - this.saveCheckboxes(this.rootNode); + this.saveCheckboxes(this.nodes); if (typeof TYPO3.FormEngine.Validation !== 'undefined' && typeof TYPO3.FormEngine.Validation.validate === 'function') { TYPO3.FormEngine.Validation.validate(); } @@ -172,7 +173,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg } node.eachAfter(function (n) { - if ((n.data.checked || n.indeterminate) && n.parent) { + if ((n.checked || n.indeterminate) && n.parent) { n.parent.indeterminate = true; } }) @@ -197,7 +198,7 @@ define(['d3', 'TYPO3/CMS/Backend/FormEngine/Element/SvgTree'], function (d3, Svg if (typeof this.settings.input !== 'undefined') { var selectedNodes = this.getSelectedNodes(); this.settings.input.val(selectedNodes.map(function (d) { - return d.data.identifier + return d.identifier })); } }; diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js index 93aab43337cd39960392454618c809e0359f6709..f20b71f5bb30e430a2d6b8760be2aedeca7df74e 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SvgTree.js @@ -193,72 +193,32 @@ define(['jquery', 'd3'], function ($, d3) { $container.parent().append('<p class="text-danger">' + TYPO3.lang['tcatree.msg_save_first'] + '</p>'); return; } - if (Array.isArray(json)) { - if (json.length > 1) { - // If tree comes with multiple root nodes, add them to a new root - var tmp = { - checked: undefined, - children: [], - expandable: true, - expanded: true, - iconTag: null, - id: '', - identifier: 'root', - leaf: false, - name: '', - overlayIcon: '', - text: '', - uid: '' - }; - for (var i = 0; i < json.length; i++) { - var n = json[i]; - if (typeof n.identifier === 'undefined') { - n.identifier = n.uid; - } - if (typeof n.name === 'undefined') { - n.name = n.text; - } - if (typeof n.expandable === 'undefined') { - n.expandable = true; - } - if (typeof n.expanded === 'undefined') { - n.expanded = true; - } - if (typeof n.icon !== 'undefined') { - n.iconTag = n.icon; - } - tmp.children.push(n); - } - json = tmp; - } else { - json = json[0]; - } - } - var rootNode = d3.hierarchy(json); - d3.tree(rootNode); - - rootNode.each(function (n) { - n.open = (me.settings.expandUpToLevel !== null) ? n.depth < me.settings.expandUpToLevel : Boolean(n.expanded); - n.hasChildren = (n.children || n._children) ? 1 : 0; - n.parents = []; - n._isDragged = false; - if (n.parent) { - var x = n; - while (x && x.parent) { - if (x.parent.data.identifier) { - n.parents.push(x.parent.data.identifier); + + var nodes = Array.isArray(json) ? json : []; + nodes = nodes.map(function (node, index) { + node.open = (me.settings.expandUpToLevel !== null) ? node.depth < me.settings.expandUpToLevel : Boolean(node.expanded); + node.parents = []; + node._isDragged = false; + if (node.depth > 0) { + var currentDepth = node.depth; + for (var i = index; i >= 0; i--) { + var currentNode = nodes[i]; + if (currentNode.depth < currentDepth) { + node.parents.push(i); + currentDepth = currentNode.depth; } - x = x.parent; } } - if (typeof n.data.checked == 'undefined') { - n.data.checked = false; - me.settings.unselectableElements.push(n.data.identifier); + if (typeof node.checked == 'undefined') { + node.checked = false; + me.settings.unselectableElements.push(node.identifier); } //dispatch event - me.dispatch.call('prepareLoadedNode', me, n); + me.dispatch.call('prepareLoadedNode', me, node); + return node; }); - me.rootNode = rootNode; + + me.nodes = nodes; me.dispatch.call('loadDataAfter', me); me.prepareDataForVisibleNodes(); me.update(); @@ -274,17 +234,16 @@ define(['jquery', 'd3'], function ($, d3) { var me = this; var blacklist = {}; - this.rootNode.eachBefore(function (node) { + this.nodes.map(function (node, index) { if (!node.open) { - blacklist[node.data.identifier] = true; + blacklist[index] = true; } - }); - this.data.nodes = this.rootNode.descendantsBefore().filter(function (node) { - return node.hidden != true && !node.parents.some(function (id) { - return Boolean(blacklist[id]); - }); + this.data.nodes = this.nodes.filter(function (node) { + return node.hidden != true && !node.parents.some(function (index) { + return Boolean(blacklist[index]); + }); }); var iconHashes = []; @@ -294,33 +253,33 @@ define(['jquery', 'd3'], function ($, d3) { //delete n.children; n.x = n.depth * me.settings.indentWidth; n.y = i * me.settings.nodeHeight; - if (n.parent) { + if (n.parents[0] !== undefined) { me.data.links.push({ - source: n.parent, + source: me.nodes[n.parents[0]], target: n }); } - if (!n.iconHash && me.settings.showIcons && n.data.icon) { - n.iconHash = Math.abs(me.hashCode(n.data.icon)); + if (!n.iconHash && me.settings.showIcons && n.icon) { + n.iconHash = Math.abs(me.hashCode(n.icon)); if (iconHashes.indexOf(n.iconHash) === -1) { iconHashes.push(n.iconHash); me.data.icons.push({ identifier: n.iconHash, - icon: n.data.icon + icon: n.icon }); } - delete n.data.icon; + delete n.icon; } - if (!n.iconOverlayHash && me.settings.showIcons && n.data.overlayIcon) { - n.iconOverlayHash = Math.abs(me.hashCode(n.data.overlayIcon)); + if (!n.iconOverlayHash && me.settings.showIcons && n.overlayIcon) { + n.iconOverlayHash = Math.abs(me.hashCode(n.overlayIcon)); if (iconHashes.indexOf(n.iconOverlayHash) === -1) { iconHashes.push(n.iconOverlayHash); me.data.icons.push({ identifier: n.iconOverlayHash, - icon: n.data.overlayIcon + icon: n.overlayIcon }); } - delete n.data.overlayIcon; + delete n.overlayIcon; } }); this.svg.attr('height', this.data.nodes.length * this.settings.nodeHeight); @@ -336,7 +295,7 @@ define(['jquery', 'd3'], function ($, d3) { var visibleNodes = this.data.nodes.slice(position, position + visibleRows); var nodes = this.nodesContainer.selectAll('.node').data(visibleNodes, function (d) { - return d.data.identifier; + return d.identifier; }); // delete nodes without corresponding data @@ -493,7 +452,7 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {String} */ getNodeLabel: function (node) { - return node.data.name; + return node.name; }, /** @@ -503,7 +462,7 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {String} */ getNodeClass: function (node) { - return 'node identifier-' + node.data.identifier; + return 'node identifier-' + node.identifier; }, /** @@ -513,7 +472,7 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {String} */ getNodeTitle: function (node) { - return 'uid=' + node.data.identifier; + return 'uid=' + node.identifier; }, /** @@ -611,7 +570,7 @@ define(['jquery', 'd3'], function ($, d3) { if (!this.isNodeSelectable(node)) { return; } - var checked = node.data.checked; + var checked = node.checked; this.handleExclusiveNodeSelection(node); if (this.settings.validation && this.settings.validation.maxItems) { @@ -620,7 +579,7 @@ define(['jquery', 'd3'], function ($, d3) { return; } } - node.data.checked = !checked; + node.checked = !checked; this.dispatch.call('nodeSelectedAfter', this, node); this.update(); @@ -635,19 +594,19 @@ define(['jquery', 'd3'], function ($, d3) { handleExclusiveNodeSelection: function (node) { var exclusiveKeys = this.settings.exclusiveNodesIdentifiers.split(','), me = this; - if (this.settings.exclusiveNodesIdentifiers.length && node.data.checked === false) { - if (exclusiveKeys.indexOf('' + node.data.identifier) > -1) { + if (this.settings.exclusiveNodesIdentifiers.length && node.checked === false) { + if (exclusiveKeys.indexOf('' + node.identifier) > -1) { // this key is exclusive, so uncheck all others this.rootNode.each(function (node) { - if (node.data.checked === true) { - node.data.checked = false; + if (node.checked === true) { + node.checked = false; me.dispatch.call('nodeSelectedAfter', me, node); } }); this.exclusiveSelectedNode = node; - } else if (exclusiveKeys.indexOf('' + node.data.identifier) === -1 && this.exclusiveSelectedNode) { + } else if (exclusiveKeys.indexOf('' + node.identifier) === -1 && this.exclusiveSelectedNode) { //current node is not exclusive, but other exclusive node is already selected - this.exclusiveSelectedNode.data.checked = false; + this.exclusiveSelectedNode.checked = false; this.dispatch.call('nodeSelectedAfter', this, this.exclusiveSelectedNode); this.exclusiveSelectedNode = null; } @@ -662,7 +621,7 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {Boolean} */ isNodeSelectable: function (node) { - return !this.settings.readOnlyMode && this.settings.unselectableElements.indexOf(node.data.identifier) == -1; + return !this.settings.readOnlyMode && this.settings.unselectableElements.indexOf(node.identifier) == -1; }, /** @@ -671,14 +630,9 @@ define(['jquery', 'd3'], function ($, d3) { * @returns {Node[]} */ getSelectedNodes: function () { - var selectedNodes = []; - - this.rootNode.each(function (node) { - if (node.data.checked) { - selectedNodes.push(node) - } + return this.nodes.filter(function (node) { + return node.checked; }); - return selectedNodes; }, /** @@ -743,7 +697,7 @@ define(['jquery', 'd3'], function ($, d3) { * Expand all nodes and refresh view */ expandAll: function () { - this.rootNode.each(this.showChildren.bind(this)); + this.nodes.forEach(this.showChildren.bind(this)); this.prepareDataForVisibleNodes(); this.update(); }, @@ -752,7 +706,7 @@ define(['jquery', 'd3'], function ($, d3) { * Collapse all nodes recursively and refresh view */ collapseAll: function () { - this.rootNode.each(this.hideChildren.bind(this)); + this.nodes.forEach(this.hideChildren.bind(this)); this.prepareDataForVisibleNodes(); this.update(); } diff --git a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js index d6ebb581f0c783a7da6e7b990c61d6f61ab720a6..f86d1a8bead965198c9c7047b178245f9d41f8aa 100644 --- a/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js +++ b/typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/TreeToolbar.js @@ -146,10 +146,10 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Tooltip', 'TYPO3 var me = this, name = $(input).val(); - this.tree.rootNode.open = false; - this.tree.rootNode.eachBefore(function (node, i) { + this.tree.nodes[0].open = false; + this.tree.nodes.forEach(function (node) { var regex = new RegExp(name, 'i'); - if (regex.test(node.data.name)) { + if (regex.test(node.name)) { me.showParents(node); node.open = true; node.hidden = false; @@ -173,8 +173,8 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Tooltip', 'TYPO3 this._hideUncheckedState = !this._hideUncheckedState; if (this._hideUncheckedState) { - this.tree.rootNode.eachBefore(function (node, i) { - if (node.data.checked) { + this.tree.nodes.forEach(function (node) { + if (node.checked) { me.showParents(node); node.open = true; node.hidden = false; @@ -184,7 +184,7 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Tooltip', 'TYPO3 } }); } else { - this.tree.rootNode.eachBefore(function (node, i) { + this.tree.nodes.forEach(function (node) { node.hidden = false; }); } @@ -199,14 +199,15 @@ define(['jquery', 'TYPO3/CMS/Backend/Icons', 'TYPO3/CMS/Backend/Tooltip', 'TYPO3 * @returns {Boolean} */ TreeToolbar.prototype.showParents = function (node) { - if (!node.parent) { + if (node.parents.length === 0) { return true; } - node.parent.hidden = false; + var parent = this.tree.nodes[node.parents[0]]; + parent.hidden = false; //expand parent node - node.parent.open = true; - this.showParents(node.parent); + parent.open = true; + this.showParents(parent); }; return TreeToolbar; diff --git a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php index f3d66dd20a878e288bdb86b30bab3514ad1399dc..d9cb7486ce63d4b55abdc4496a42ed87346c024b 100644 --- a/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php +++ b/typo3/sysext/backend/Tests/Unit/Form/FormDataProvider/TcaSelectTreeItemsTest.php @@ -180,8 +180,8 @@ class TcaSelectTreeItemsTest extends UnitTestCase $expected = $input; $expected['databaseRow']['aField'] = ['1']; - $expected['processedTca']['columns']['aField']['config']['treeData'] = [ - 'items' => [['fake', 'tree', 'data']], + $expected['processedTca']['columns']['aField']['config']['items'] = [ + 'fake', 'tree', 'data', ]; $this->assertEquals($expected, $this->subject->addData($input)); } @@ -207,6 +207,9 @@ class TcaSelectTreeItemsTest extends UnitTestCase /** @var TableConfigurationTree|ObjectProphecy $treeDataProviderProphecy */ $tableConfigurationTreeProphecy = $this->prophesize(TableConfigurationTree::class); GeneralUtility::addInstance(TableConfigurationTree::class, $tableConfigurationTreeProphecy->reveal()); + $tableConfigurationTreeProphecy->render()->willReturn([]); + $tableConfigurationTreeProphecy->setDataProvider(Argument::cetera())->shouldBeCalled(); + $tableConfigurationTreeProphecy->setNodeRenderer(Argument::cetera())->shouldBeCalled(); $input = [ 'tableName' => 'aTable', diff --git a/typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php b/typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php index 326a495358ee5f5d231f2f9523a4d7e408a82257..e7eaa351cb5a4796eb23d3bcae969c045974fa96 100644 --- a/typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php +++ b/typo3/sysext/core/Classes/Tree/TableConfiguration/ExtJsArrayTreeRenderer.php @@ -19,27 +19,6 @@ namespace TYPO3\CMS\Core\Tree\TableConfiguration; */ class ExtJsArrayTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\ExtJsJsonTreeRenderer { - /** - * Gets the node array. If the TCA configuration has defined items, - * they are added to rootlevel on top of the tree - * - * @param \TYPO3\CMS\Backend\Tree\TreeRepresentationNode|DatabaseTreeNode $node - * @return array - */ - protected function getNodeArray(\TYPO3\CMS\Backend\Tree\TreeRepresentationNode $node) - { - $nodeArray = parent::getNodeArray($node); - $nodeArray = array_merge($nodeArray, [ - 'expanded' => $node->getExpanded(), - 'expandable' => $node->hasChildNodes(), - 'checked' => $node->getSelected() - ]); - if (!$node->getSelectable()) { - unset($nodeArray['checked']); - } - return $nodeArray; - } - /** * Renders a node collection recursive or just a single instance * @@ -50,7 +29,6 @@ class ExtJsArrayTreeRenderer extends \TYPO3\CMS\Backend\Tree\Renderer\ExtJsJsonT public function renderTree(\TYPO3\CMS\Backend\Tree\AbstractTree $tree, $recursive = true) { $this->recursionLevel = 0; - $children = $this->renderNode($tree->getRoot(), $recursive); - return $children; + return $this->renderNode($tree->getRoot(), $recursive); } } diff --git a/typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php b/typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php index c78ce41058a2764d5907df5ebe3488ddc8ec4da9..bf11ae3d5f105592a0a457160920f5d1edb5f717 100644 --- a/typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php +++ b/typo3/sysext/core/Classes/Tree/TableConfiguration/TreeDataProviderFactory.php @@ -26,7 +26,7 @@ class TreeDataProviderFactory * @param array $tcaConfiguration * @param $table * @param $field - * @param $currentValue + * @param array $currentValue The current database row, handing over 'uid' is enough * @return DatabaseTreeDataProvider * @throws \InvalidArgumentException */ @@ -45,7 +45,6 @@ class TreeDataProviderFactory $tcaConfiguration['internal_type'] = 'db'; } if ($tcaConfiguration['internal_type'] === 'db') { - $unselectableUids = []; if ($dataProvider === null) { $dataProvider = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Tree\TableConfiguration\DatabaseTreeDataProvider::class); } @@ -53,7 +52,9 @@ class TreeDataProviderFactory $tableName = $tcaConfiguration['foreign_table']; $dataProvider->setTableName($tableName); if ($tableName == $table) { - $unselectableUids[] = $currentValue['uid']; + // The uid of the currently opened row can not be selected in a table relation to "self" + $unselectableUids = [ $currentValue['uid'] ]; + $dataProvider->setItemUnselectableList($unselectableUids); } } else { throw new \InvalidArgumentException('TCA Tree configuration is invalid: "foreign_table" not set', 1288215888); @@ -64,7 +65,6 @@ class TreeDataProviderFactory $dataProvider->setLabelField($GLOBALS['TCA'][$tableName]['ctrl']['label']); } $dataProvider->setTreeId(md5($table . '|' . $field)); - $dataProvider->setSelectedList($currentValue); $treeConfiguration = $tcaConfiguration['treeConfig']; if (isset($treeConfiguration['rootUid'])) { @@ -90,7 +90,6 @@ class TreeDataProviderFactory } else { throw new \InvalidArgumentException('TCA Tree configuration is invalid: neither "childrenField" nor "parentField" is set', 1288215889); } - $dataProvider->setItemUnselectableList($unselectableUids); } elseif ($tcaConfiguration['internal_type'] === 'file' && $dataProvider === null) { // @todo Not implemented yet throw new \InvalidArgumentException('TCA Tree configuration is invalid: tree for "internal_type=file" not implemented yet', 1288215891);