Commit cfd73ed3 authored by Christian Kuhn's avatar Christian Kuhn
Browse files

[TASK] TCA tree: Simplify json result

The patch changes the ajax result that delivers TCA tree
items to the SVG tree from a nested list of items to a
sorted flat list having a 'depth' argument to indicate the
nesting level.
This "flat" list is the native mode of the d3 tree, with
this change the JS side can be streamlined quite a bit.
Along the way, the item providing on PHP side is streamlined,
documented much better and easier to understand now within the
data provider of FormEngine.
The main tree data backend is still a huge, convoluted, slow
and insane mess that will eventually fully substituted with a
much straighter and quicker approach later. Changes in this area
are kept to a minimum for now.

Change-Id: Ib64b7277f671b632be3977218e5465b534618d63
Resolves: #78905
Related: #76108
Releases: master
Reviewed-on: https://review.typo3.org/50813

Reviewed-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Tested-by: Wouter Wolters's avatarWouter Wolters <typo3@wouterwolters.nl>
Tested-by: default avatarTYPO3com <no-reply@typo3.com>
Reviewed-by: default avatarThomas Maroschik <tmaroschik@dfau.de>
Tested-by: default avatarThomas Maroschik <tmaroschik@dfau.de>
Reviewed-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn's avatarChristian Kuhn <lolli@schwarzbu.ch>
parent 52dd9357
......@@ -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;
}
......
......@@ -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) {
......
......@@ -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
*
......
......@@ -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;
}
......
......@@ -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
}));
}
};
......
......@@ -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) {