blob: b9e301c36283b8e436f0e99064a035d7484899d2 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014-2018 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
/**
* @class
* @constructor
*/
scout.Tree = function() {
scout.Tree.parent.call(this);
this.toggleBreadcrumbStyleEnabled = false;
this.autoCheckChildren = false;
this.checkable = false;
this.checkableStyle = scout.Tree.CheckableStyle.CHECKBOX_TREE_NODE;
this.displayStyle = scout.Tree.DisplayStyle.DEFAULT;
this.dropType = 0;
this.dropMaximumSize = scout.dragAndDrop.DEFAULT_DROP_MAXIMUM_SIZE;
this.enabled = true;
this.filterEnabled = false;
this.lazyExpandingEnabled = true;
this.menus = [];
this.contextMenu = null;
this.menuBar = null;
this.keyStrokes = [];
this.multiCheck = true;
this.nodes = []; // top-level nodes
this.nodesMap = {}; // all nodes by id
this.nodePaddingLevelCheckable = 23; /* padding for one tree-level if the tree is checkable */
this.nodePaddingLevelNotCheckable = 18; /* padding for one tree-level if the tree is not checkable. this includes outline trees! */
this.nodePaddingLeft = null; /* is read from CSS */
this.nodeCheckBoxPaddingLeft = 29;
this.nodeControlPaddingLeft = null; /* is read from CSS */
this.nodePaddingLevel = this.nodePaddingLevelNotCheckable;
this.scrollToSelection = false;
this.scrollTop = 0;
this.scrollTopHistory = []; // Only necessary for breadcrumb mode
this.selectedNodes = [];
this.prevSelectedNode = null; // The previously selected node, relevant for breadcrumb in compact mode
// performance optimization: E.g. rather than iterating over the whole tree when unchecking all nodes,
// we explicitly keep track of nodes to uncheck (useful e.g. for single-check mode in very large trees).
this.checkedNodes = [];
this.groupedNodes = {};
this.visibleNodesFlat = [];
this.visibleNodesMap = {};
this._addWidgetProperties(['menus', 'keyStrokes']);
this._additionalContainerClasses = ''; // may be used by subclasses to set additional CSS classes
this._filters = [];
this._doubleClickSupport = new scout.DoubleClickSupport();
this._$animationWrapper = null; // used by _renderExpansion()
this._$expandAnimationWrappers = [];
this._filterMenusHandler = this._filterMenus.bind(this);
this._popupOpenHandler = this._onDesktopPopupOpen.bind(this);
// contains all parents of a selected node, the selected node and the first level children
this._inSelectionPathList = {};
this.viewRangeRendered = new scout.Range(0, 0);
this.viewRangeSize = 20;
this.startAnimationFunc = function() {
this.runningAnimations++;
}.bind(this);
this.runningAnimations = 0;
this.runningAnimationsFinishFunc = function() {
this.runningAnimations--;
if (this.runningAnimations <= 0) {
this.runningAnimations = 0;
this._renderViewportBlocked = false;
this.invalidateLayoutTree();
}
}.bind(this);
this.nodeHeight = 0;
this.nodeWidth = 0;
this.maxNodeWidth = 0;
this.nodeWidthDirty = false;
this.$data = null;
this._scrolldirections = 'both';
this.requestFocusOnNodeControlMouseDown = true;
};
scout.inherits(scout.Tree, scout.Widget);
scout.Tree.DisplayStyle = {
DEFAULT: 'default',
BREADCRUMB: 'breadcrumb'
};
scout.Tree.CheckableStyle = {
/**
* Node check is only possible by checking the checkbox.
*/
CHECKBOX: 'checkbox',
/**
* Node check is possible by clicking anywhere on the node.
*/
CHECKBOX_TREE_NODE: 'checkbox_tree_node'
};
scout.Tree.prototype._init = function(model) {
scout.Tree.parent.prototype._init.call(this, model);
this.addFilter(new scout.LazyNodeFilter(this), true);
this.breadcrumbFilter = new scout.TreeBreadcrumbFilter(this);
if (this.displayStyle === scout.Tree.DisplayStyle.BREADCRUMB) {
this.addFilter(this.breadcrumbFilter, true, true);
}
this.initialTraversing = true;
this._setCheckable(this.checkable);
this._ensureTreeNodes(this.nodes);
this.visitNodes(this._initTreeNode.bind(this));
this.visitNodes(this._updateFlatListAndSelectionPath.bind(this));
this.initialTraversing = false;
this.selectedNodes = this._nodesByIds(this.selectedNodes);
this.menuBar = scout.create('MenuBar', {
parent: this,
menuOrder: new scout.MenuItemsOrder(this.session, 'Tree'),
menuFilter: this._filterMenusHandler
});
this.menuBar.bottom();
this._updateItemPath(true);
this._setDisplayStyle(this.displayStyle);
this._setKeyStrokes(this.keyStrokes);
this._setMenus(this.menus);
};
/**
* Iterates through the given array and converts node-models to instances of scout.TreeNode (or a subclass).
* If the array element is already a TreeNode the function leaves the element untouched. This function also
* ensures that the attribute childNodeIndex is set. By default we use the order of the nodes array as index
* but only if childNodeIndex is undefined.
*
* @param nodes Array of node-models (plain object) or nodes (instance of scout.TreeNode)
*/
scout.Tree.prototype._ensureTreeNodes = function(nodes) {
var i, node;
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
node.childNodeIndex = scout.nvl(node.childNodeIndex, i);
if (node instanceof scout.TreeNode) {
continue;
}
nodes[i] = this._createTreeNode(node);
}
};
scout.Tree.prototype._createTreeNode = function(nodeModel) {
nodeModel = scout.nvl(nodeModel, {});
nodeModel.parent = this;
return scout.create('TreeNode', nodeModel);
};
/**
* @override
*/
scout.Tree.prototype._createKeyStrokeContext = function() {
return new scout.KeyStrokeContext();
};
/**
* @override
*/
scout.Tree.prototype._initKeyStrokeContext = function() {
scout.Tree.parent.prototype._initKeyStrokeContext.call(this);
this._initTreeKeyStrokeContext();
};
scout.Tree.prototype._initTreeKeyStrokeContext = function() {
var modifierBitMask = scout.keyStrokeModifier.NONE;
this.keyStrokeContext.registerKeyStroke([
new scout.TreeSpaceKeyStroke(this),
new scout.TreeNavigationUpKeyStroke(this, modifierBitMask),
new scout.TreeNavigationDownKeyStroke(this, modifierBitMask),
new scout.TreeCollapseAllKeyStroke(this, modifierBitMask),
new scout.TreeCollapseOrDrillUpKeyStroke(this, modifierBitMask),
new scout.TreeNavigationEndKeyStroke(this, modifierBitMask),
new scout.TreeExpandOrDrillDownKeyStroke(this, modifierBitMask)
]);
// Prevent default action and do not propagate ↓ or ↑ keys if ctrl- or alt-modifier is not pressed.
// Otherwise, an '↑-event' on the first node, or an '↓-event' on the last row will bubble up (because not consumed by tree navigation keystrokes) and cause a superior tree to move its selection;
// Use case: - outline tree with a detail form that contains a tree;
// - preventDefault because of smartfield, so that the cursor is not moved on first or last row;
this.keyStrokeContext.registerStopPropagationInterceptor(function(event) {
if (!event.ctrlKey && !event.altKey && scout.isOneOf(event.which, scout.keys.UP, scout.keys.DOWN)) {
event.stopPropagation();
event.preventDefault();
}
});
};
scout.Tree.prototype._setMenus = function(menus) {
this.updateKeyStrokes(menus, this.menus);
this._setProperty('menus', menus);
this._updateMenuBar();
};
scout.Tree.prototype._updateMenuBar = function() {
var menuItems = this._filterMenus(this.menus, scout.MenuDestinations.MENU_BAR, false, true);
this.menuBar.setMenuItems(menuItems);
var contextMenuItems = this._filterMenus(this.menus, scout.MenuDestinations.CONTEXT_MENU, true);
if (this.contextMenu) {
this.contextMenu.updateMenuItems(contextMenuItems);
}
};
scout.Tree.prototype._setKeyStrokes = function(keyStrokes) {
this.updateKeyStrokes(keyStrokes, this.keyStrokes);
this._setProperty('keyStrokes', keyStrokes);
};
scout.Tree.prototype._resetTreeNode = function(node, parentNode) {
node.reset();
};
scout.Tree.prototype.isSelectedNode = function(node) {
if (this.initialTraversing) {
return this.selectedNodes.indexOf(node.id) > -1;
} else {
return this.selectedNodes.indexOf(node) > -1;
}
};
scout.Tree.prototype._updateFlatListAndSelectionPath = function(node, parentNode) {
// if this node is selected all parent nodes have to be added to selectionPath
if (this.isSelectedNode(node) && ((node.parentNode && !this.visibleNodesMap[node.parentNode.id]) || node.level === 0)) {
var p = node;
while (p) {
this._inSelectionPathList[p.id] = true;
p.filterDirty = true;
if (p !== node) {
// ensure node is expanded
node.expanded = true;
// if parent was filtered before, try refilter after adding to selection path.
if (p.level === 0) {
this._applyFiltersForNode(p);
// add visible nodes to visible nodes array when they are initialized
this._addToVisibleFlatList(p, false);
// process children
this._addChildrenToFlatList(p, this.visibleNodesFlat.length - 1, false, null, true);
}
}
p = p.parentNode;
}
} else if (node.parentNode && this.isSelectedNode(node.parentNode)) {
this._inSelectionPathList[node.id] = true;
}
this._applyFiltersForNode(node);
// add visible nodes to visible nodes array when they are initialized
this._addToVisibleFlatList(node, false);
};
scout.Tree.prototype._initTreeNode = function(node, parentNode) {
this.nodesMap[node.id] = node;
if (parentNode) {
node.parentNode = parentNode;
node.level = node.parentNode.level + 1;
}
if (node.checked) {
this.checkedNodes.push(node);
}
this._initTreeNodeInternal(node, parentNode);
this._updateMarkChildrenChecked(node, true, node.checked);
node.initialized = true;
};
scout.Tree.prototype._applyNodeDefaultValues = function(node) {
scout.defaultValues.applyTo(node, 'TreeNode');
};
/**
* Override this function if you want a custom node init before filtering.
* The default impl. applies default values to the given node.
*/
scout.Tree.prototype._initTreeNodeInternal = function(node, parentNode) {
this._applyNodeDefaultValues(node);
};
scout.Tree.prototype._destroy = function() {
scout.Tree.parent.prototype._destroy.call(this);
this.visitNodes(this._destroyTreeNode.bind(this));
this.nodes = []; // finally, clear array with root tree-nodes
};
scout.Tree.prototype._destroyTreeNode = function(node, parentNode) {
delete this.nodesMap[node.id];
scout.arrays.remove(this.selectedNodes, node); // ensure deleted node is not in selection list anymore (in case the model does not update the selection)
scout.arrays.remove(this.checkedNodes, node); // ensure deleted node is not in checked list anymore
this._removeFromFlatList(node, false); // ensure node is not longer in visible nodes list.
node.destroy();
if (this._onNodeDeleted) { // Necessary for subclasses
this._onNodeDeleted(node);
}
};
/**
* pre-order (top-down) traversal of the tree-nodes of this tree.<br>
* if func returns true the children of the visited node are not visited.
*/
scout.Tree.prototype.visitNodes = function(func, parentNode) {
return scout.Tree.visitNodes(func, this.nodes, parentNode);
};
scout.Tree.prototype._render = function() {
this.$container = this.$parent.appendDiv('tree');
if (this._additionalContainerClasses) {
this.$container.addClass(this._additionalContainerClasses);
}
var layout = new scout.TreeLayout(this);
this.htmlComp = scout.HtmlComponent.install(this.$container, this.session);
this.htmlComp.setLayout(layout);
this.htmlComp.pixelBasedSizing = false;
this.$data = this.$container.appendDiv('tree-data')
.on('contextmenu', this._onContextMenu.bind(this))
.on('mousedown', '.tree-node', this._onNodeMouseDown.bind(this))
.on('mouseup', '.tree-node', this._onNodeMouseUp.bind(this))
.on('dblclick', '.tree-node', this._onNodeDoubleClick.bind(this))
.on('mousedown', '.tree-node-control', this._onNodeControlMouseDown.bind(this))
.on('mouseup', '.tree-node-control', this._onNodeControlMouseUp.bind(this))
.on('dblclick', '.tree-node-control', this._onNodeControlDoubleClick.bind(this));
scout.HtmlComponent.install(this.$data, this.session);
if (this.isHorizontalScrollingEnabled()) {
this.$data.toggleClass('scrollable-tree', true);
}
this._installScrollbars({
axis: this._scrolldirections
});
this._installNodeTooltipSupport();
this.menuBar.render();
this._updateNodeDimensions();
// render display style before viewport (not in renderProperties) to have a correct style from the beginning
this._renderDisplayStyle();
this._renderViewport();
this.session.desktop.on('popupOpen', this._popupOpenHandler);
this._renderCheckableStyle();
};
scout.Tree.prototype._postRender = function() {
scout.Tree.parent.prototype._postRender.call(this);
this._renderSelection();
};
scout.Tree.prototype._remove = function() {
// remove listener
this.session.desktop.off('popupOpen', this._popupOpenHandler);
// stop all animations
if (this._$animationWrapper) {
this._$animationWrapper.stop(false, true);
}
// Detach nodes from jQuery objects (because those will be removed)
this.visitNodes(this._resetTreeNode.bind(this));
this._uninstallDragAndDropHandler();
this._uninstallNodeTooltipSupport();
this.$fillBefore = null;
this.$fillAfter = null;
this.$data = null;
// reset rendered view range because now range is rendered
this.viewRangeRendered = new scout.Range(0, 0);
scout.Tree.parent.prototype._remove.call(this);
};
scout.Tree.prototype._renderProperties = function() {
scout.Tree.parent.prototype._renderProperties.call(this);
this._renderDropType();
};
scout.Tree.prototype.isHorizontalScrollingEnabled = function() {
return this._scrolldirections === 'both' || this._scrolldirections === 'x';
};
scout.Tree.prototype.isTreeNodeCheckEnabled = function() {
return this.checkableStyle === scout.Tree.CheckableStyle.CHECKBOX_TREE_NODE;
};
/**
* @override
*/
scout.Tree.prototype._onScroll = function() {
var scrollToSelectionBackup = this.scrollToSelection;
this.scrollToSelection = false;
var scrollTop = this.$data[0].scrollTop;
var scrollLeft = this.$data[0].scrollLeft;
if (this.scrollTop !== scrollTop) {
this._renderViewport();
}
this.scrollTop = scrollTop;
this.scrollLeft = scrollLeft;
this.scrollToSelection = scrollToSelectionBackup;
};
/**
* @override
*/
scout.Tree.prototype.setScrollTop = function(scrollTop) {
this.setProperty('scrollTop', scrollTop);
// call _renderViewport to make sure nodes are rendered immediately. The browser fires the scroll event handled by onDataScroll delayed
if (this.rendered) {
this._renderViewport();
// Render scroll top again to make sure the data really is at the expected position
// This seems only to be necessary for Chrome and the tree, it seems to work for IE and table.
// It is not optimal, because actually it should be possible to modify the $data[0].scrollTop without using this function
// Some debugging showed that after reducing the height of the afterFiller in _renderFiller the scrollTop will be wrong.
// Updating the scrollTop in renderFiller or other view range relevant function is bad because it corrupts smooth scrolling (see also commit c14ce92e0a7bff568d4f2d715e3061a782e728c2)
this._renderScrollTop();
}
};
/**
* @override
*/
scout.Tree.prototype._renderScrollTop = function() {
if (this.rendering) {
// Not necessary to do it while rendering since it will be done by the layout
return;
}
scout.scrollbars.scrollTop(this.$data, this.scrollTop);
};
/**
* @override
*/
scout.Tree.prototype.get$Scrollable = function() {
return this.$data;
};
scout.Tree.prototype._renderViewport = function() {
if (this.runningAnimations > 0 || this._renderViewportBlocked) {
// animation pending do not render view port because finishing should rerenderViewport
return;
}
if (!this.$container.isEveryParentVisible()) {
// If the tree is invisible, the width and height of the nodes cannot be determined
// In that case, the tree won't be layouted either -> as soon as it will be layouted, renderViewport will be called again
return;
}
var viewRange = this._calculateCurrentViewRange();
this._renderViewRange(viewRange);
};
scout.Tree.prototype._calculateCurrentViewRange = function() {
var node,
scrollTop = this.$data[0].scrollTop,
maxScrollTop = this.$data[0].scrollHeight - this.$data[0].clientHeight;
if (maxScrollTop === 0 && this.visibleNodesFlat.length > 0) {
// no scrollbars visible
node = this.visibleNodesFlat[0];
} else {
node = this._nodeAtScrollTop(scrollTop);
}
return this._calculateViewRangeForNode(node);
};
scout.Tree.prototype._rerenderViewport = function() {
if (this._renderViewportBlocked) {
return;
}
this._removeRenderedNodes();
this._renderFiller();
this._updateDomNodeWidth();
this._updateDomNodeIconWidth();
this._renderViewport();
};
scout.Tree.prototype._removeRenderedNodes = function() {
var $nodes = this.$data.find('.tree-node');
$nodes.each(function(i, elem) {
var $node = $(elem),
node = $node.data('node');
if ($node.hasClass('hiding')) {
// Do not remove nodes which are removed using an animation
return;
}
this._removeNode(node);
}.bind(this));
this.viewRangeRendered = new scout.Range(0, 0);
};
scout.Tree.prototype._renderViewRangeForNode = function(node) {
var viewRange = this._calculateViewRangeForNode(node);
this._renderViewRange(viewRange);
};
scout.Tree.prototype._renderNodesInRange = function(range) {
var prepend = false;
var nodes = this.visibleNodesFlat;
if (nodes.length === 0) {
return;
}
var maxRange = new scout.Range(0, nodes.length);
range = maxRange.intersect(range);
if (this.viewRangeRendered.size() > 0 && !range.intersect(this.viewRangeRendered).equals(new scout.Range(0, 0))) {
throw new Error('New range must not intersect with existing.');
}
if (range.to <= this.viewRangeRendered.from) {
prepend = true;
}
var newRange = this.viewRangeRendered.union(range);
if (newRange.length === 2) {
throw new Error('Can only prepend or append rows to the existing range. Existing: ' + this.viewRangeRendered + '. New: ' + newRange);
}
this.viewRangeRendered = newRange[0];
var numNodesRendered = this.ensureRangeVisible(range);
$.log.isTraceEnabled() && $.log.trace(numNodesRendered + ' new nodes rendered from ' + range);
};
scout.Tree.prototype.ensureRangeVisible = function(range) {
var nodes = this.visibleNodesFlat;
var nodesToInsert = [];
for (var r = range.from; r < range.to; r++) {
var node = nodes[r];
if (!node.attached) {
nodesToInsert.push(node);
}
}
this._insertNodesInDOM(nodesToInsert);
return nodesToInsert.length;
};
scout.Tree.prototype._renderFiller = function() {
if (!this.$fillBefore) {
this.$fillBefore = this.$data.prependDiv('tree-data-fill');
}
var fillBeforeDimensions = this._calculateFillerDimension(new scout.Range(0, this.viewRangeRendered.from));
this.$fillBefore.cssHeight(fillBeforeDimensions.height);
if (this.isHorizontalScrollingEnabled()) {
this.$fillBefore.cssWidth(fillBeforeDimensions.width);
this.maxNodeWidth = Math.max(fillBeforeDimensions.width, this.maxNodeWidth);
}
$.log.isTraceEnabled() && $.log.trace('FillBefore height: ' + fillBeforeDimensions.height);
if (!this.$fillAfter) {
this.$fillAfter = this.$data.appendDiv('tree-data-fill');
}
var fillAfterDimensions = {
height: 0,
width: 0
};
fillAfterDimensions = this._calculateFillerDimension(new scout.Range(this.viewRangeRendered.to, this.visibleNodesFlat.length));
this.$fillAfter.cssHeight(fillAfterDimensions.height);
if (this.isHorizontalScrollingEnabled()) {
this.$fillAfter.cssWidth(fillAfterDimensions.width);
this.maxNodeWidth = Math.max(fillAfterDimensions.width, this.maxNodeWidth);
}
$.log.isTraceEnabled() && $.log.trace('FillAfter height: ' + fillAfterDimensions.height);
};
scout.Tree.prototype._calculateFillerDimension = function(range) {
var outerWidth = 0;
if (this.rendered) {
// the outer-width is only correct if this tree is already rendered. otherwise wrong values are returned.
outerWidth = this.$data.outerWidth();
}
var dimension = {
height: 0,
width: Math.max(outerWidth, this.maxNodeWidth)
};
for (var i = range.from; i < range.to; i++) {
var node = this.visibleNodesFlat[i];
dimension.height += this._heightForNode(node);
dimension.width = Math.max(dimension.width, this._widthForNode(node));
}
return dimension;
};
scout.Tree.prototype._removeNodesInRange = function(range) {
var fromNode, toNode, node, i,
numNodesRemoved = 0,
nodes = this.visibleNodesFlat;
var maxRange = new scout.Range(0, nodes.length);
range = maxRange.intersect(range);
fromNode = nodes[range.from];
toNode = nodes[range.to];
var newRange = this.viewRangeRendered.subtract(range);
if (newRange.length === 2) {
throw new Error('Can only remove nodes at the beginning or end of the existing range. ' + this.viewRangeRendered + '. New: ' + newRange);
}
this.viewRangeRendered = newRange[0];
for (i = range.from; i < range.to; i++) {
node = nodes[i];
this._removeNode(node);
numNodesRemoved++;
}
$.log.isTraceEnabled() && $.log.trace(numNodesRemoved + ' nodes removed from ' + range + '.');
};
/**
* Just removes the node, does NOT adjust this.viewRangeRendered
*/
scout.Tree.prototype._removeNode = function(node) {
var $node = node.$node;
if (!$node) {
return;
}
if ($node.hasClass('hiding')) {
// Do not remove nodes which are removed using an animation
return;
}
//only remove node
$node.detach();
node.attached = false;
};
/**
* Renders the rows visible in the viewport and removes the other rows
*/
scout.Tree.prototype._renderViewRange = function(viewRange) {
if (viewRange.from === this.viewRangeRendered.from && viewRange.to === this.viewRangeRendered.to && !this.viewRangeDirty) {
// When node with has changed (because of changes in layout) we must at least
// update the internal node width even though the view-range has not changed.
if (this.nodeWidthDirty) {
this._renderFiller();
this._updateDomNodeWidth();
this._updateDomNodeIconWidth();
}
// Range already rendered -> do nothing
return;
}
if (!this.viewRangeDirty) {
var rangesToRender = viewRange.subtract(this.viewRangeRendered);
var rangesToRemove = this.viewRangeRendered.subtract(viewRange);
var maxRange = new scout.Range(0, this.visibleNodesFlat.length);
rangesToRemove.forEach(function(range) {
this._removeNodesInRange(range);
if (maxRange.to < range.to) {
this.viewRangeRendered = viewRange;
}
}.bind(this));
rangesToRender.forEach(function(range) {
this._renderNodesInRange(range);
}.bind(this));
} else {
//expansion changed
this.viewRangeRendered = viewRange;
this.ensureRangeVisible(viewRange);
}
// check if at least last and first row in range got correctly rendered
if (this.viewRangeRendered.size() > 0) {
var nodes = this.visibleNodesFlat;
var firstNode = nodes[this.viewRangeRendered.from];
var lastNode = nodes[this.viewRangeRendered.to - 1];
if (this.viewRangeDirty) {
// cleanup nodes before range and after
var $nodesBeforFirstNode = firstNode.$node.prevAll('.tree-node');
var $nodesAfterLastNode = lastNode.$node.nextAll('.tree-node');
this._cleanupNodes($nodesBeforFirstNode);
this._cleanupNodes($nodesAfterLastNode);
}
if (!firstNode.attached || !lastNode.attached) {
throw new Error('Nodes not rendered as expected. ' + this.viewRangeRendered + '. First: ' + firstNode.$node + '. Last: ' + lastNode.$node);
}
}
this._postRenderViewRange();
this.viewRangeDirty = false;
};
scout.Tree.prototype._postRenderViewRange = function() {
this._renderFiller();
this._updateDomNodeWidth();
this._updateDomNodeIconWidth();
this._renderSelection();
};
/**
* The handling of the icon-size here depends on two assumptions:
*
* 1. font icons are always pre-loaded on application startup. This means outerWidth() will always return the correct
* size of the icon at any time.
*
* 2. bitmap icons are not pre-loaded. This means, when the icon is shown, the size can be unknown because the
* browser has not yet loaded the image resource. Because of that outerWidth() could not return the correct size
* and also layout would have trouble. Because in a tree all icons should have the same size, we simply define
* the min-width and min-height of bitmap icons by CSS. So we always have a proper value when we read the icon
* size. We don't support the case where the same tree has bitmap icons in different sizes. When someone needs
* larger icons, one could simple change the global constant @tree-node-bitmap-icon-size to change the icon size
* for all trees, or set a CSS rule/class when only a single tree must have a different icon size.
*/
scout.Tree.prototype._updateDomNodeIconWidth = function($nodes) {
if (!this.rendered && !this.rendering) {
return;
}
this._visibleNodesInViewRange().forEach(function(node) {
node._updateIconWidth();
});
};
scout.Tree.prototype._visibleNodesInViewRange = function() {
return this.visibleNodesFlat.slice(this.viewRangeRendered.from, this.viewRangeRendered.to);
};
scout.Tree.prototype._updateDomNodeWidth = function() {
if (!this.isHorizontalScrollingEnabled()) {
return;
}
if (!this.rendered || !this.nodeWidthDirty) {
return;
}
var nodes = this._visibleNodesInViewRange();
var maxNodeWidth = this.maxNodeWidth;
// find max-width
maxNodeWidth = nodes.reduce(function(aggr, node) {
return Math.max(node.width, aggr);
}, scout.nvl(maxNodeWidth, 0));
// set max width on all nodes
nodes.forEach(function(node) {
node.$node.cssWidth(maxNodeWidth);
});
this.nodeWidthDirty = false;
};
scout.Tree.prototype._cleanupNodes = function($nodes) {
for (var i = 0; i < $nodes.length; i++) {
this._removeNode($nodes.eq(i).data('node'));
}
};
/**
* Returns the index of the node which is at position scrollTop.
*/
scout.Tree.prototype._nodeAtScrollTop = function(scrollTop) {
var height = 0,
nodeTop;
this.visibleNodesFlat.some(function(node, i) {
height += this._heightForNode(node);
if (scrollTop < height) {
nodeTop = node;
return true;
}
}.bind(this));
var visibleNodesLength = this.visibleNodesFlat.length;
if (!nodeTop && visibleNodesLength > 0) {
nodeTop = this.visibleNodesFlat[visibleNodesLength - 1];
}
return nodeTop;
};
scout.Tree.prototype._heightForNode = function(node) {
var height = 0;
if (node.height) {
height = node.height;
} else {
height = this.nodeHeight;
}
return height;
};
scout.Tree.prototype._widthForNode = function(node) {
var width = 0;
if (node.width) {
width = node.width;
} else {
width = this.nodeWidth;
}
return width;
};
/**
* Returns a range of size this.viewRangeSize. Start of range is nodeIndex - viewRangeSize / 4.
* -> 1/4 of the nodes are before the viewport 2/4 in the viewport 1/4 after the viewport,
* assuming viewRangeSize is 2*number of possible nodes in the viewport (see calculateViewRangeSize).
*/
scout.Tree.prototype._calculateViewRangeForNode = function(node) {
var viewRange = new scout.Range(),
quarterRange = Math.floor(this.viewRangeSize / 4),
diff;
var nodeIndex = this.visibleNodesFlat.indexOf(node);
viewRange.from = Math.max(nodeIndex - quarterRange, 0);
viewRange.to = Math.min(viewRange.from + this.viewRangeSize, this.visibleNodesFlat.length);
if (!node || nodeIndex === -1) {
return viewRange;
}
// Try to use the whole viewRangeSize (extend from if necessary)
diff = this.viewRangeSize - viewRange.size();
if (diff > 0) {
viewRange.from = Math.max(viewRange.to - this.viewRangeSize, 0);
}
return viewRange;
};
/**
* Calculates the optimal view range size (number of nodes to be rendered).
* It uses the default node height to estimate how many nodes fit in the view port.
* The view range size is this value * 2.
*/
scout.Tree.prototype.calculateViewRangeSize = function() {
// Make sure row height is up to date (row height may be different after zooming)
this._updateNodeDimensions();
if (this.nodeHeight === 0) {
throw new Error('Cannot calculate view range with nodeHeight = 0');
}
return Math.ceil(this.$data.outerHeight() / this.nodeHeight) * 2;
};
scout.Tree.prototype.setViewRangeSize = function(viewRangeSize) {
if (this.viewRangeSize === viewRangeSize) {
return;
}
this._setProperty('viewRangeSize', viewRangeSize);
if (this.rendered) {
this._renderViewport();
}
};
scout.Tree.prototype._updateNodeDimensions = function() {
// var emptyNode = this._createTreeNode();
// var $node = this._renderNode(emptyNode).appendTo(this.$data);
// this.nodeHeight = $node.outerHeight(true);
// if (this.isHorizontalScrollingEnabled()) {
// var oldNodeWidth = this.nodeWidth;
// this.nodeWidth = $node.outerWidth(true);
// if (oldNodeWidth !== this.nodeWidth) {
// this.viewRangeDirty = true;
// }
// }
// emptyNode.reset();
this.nodeHeight = 30;
};
/**
* Updates the node heights for every visible node and clears the height of the others
*/
scout.Tree.prototype.updateNodeHeights = function() {
this.visibleNodesFlat.forEach(function(node) {
if (!node.attached) {
node.height = null;
} else {
node.height = node.$node.outerHeight(true);
}
});
};
scout.Tree.prototype.removeAllNodes = function() {
this._removeNodes(this.nodes);
};
/**
* @param parentNode optional. If provided, this node's state will be updated (e.g. it will be collapsed)
*/
scout.Tree.prototype._removeNodes = function(nodes, parentNode) {
if (nodes.length === 0) {
return;
}
nodes.forEach(function(node) {
this._removeFromFlatList(node, true);
if (node.childNodes.length > 0) {
this._removeNodes(node.childNodes, node);
}
if (node.$node) {
if (this._$animationWrapper && this._$animationWrapper.find(node.$node).length > 0) {
this._$animationWrapper.stop(false, true);
}
node.reset();
}
}, this);
//If every child node was deleted mark node as collapsed (independent of the model state)
//--> makes it consistent with addNodes and expand (expansion is not allowed if there are no child nodes)
var $parentNode = (parentNode ? parentNode.$node : undefined);
if ($parentNode) {
var childNodesOfParent = parentNode.childNodes;
if (!childNodesOfParent || childNodesOfParent.length === 0) {
$parentNode.removeClass('expanded');
$parentNode.removeClass('lazy');
}
}
if (this.rendered) {
this.viewRangeDirty = true;
this.invalidateLayoutTree();
}
};
scout.Tree.prototype._renderNode = function(node) {
var paddingLeft = this._computeNodePaddingLeft(node);
node.render(this.$container, paddingLeft, this.checkable, this.enabled);
return node.$node;
};
scout.Tree.prototype._removeMenus = function() {
// menubar takes care about removal
};
scout.Tree.prototype._filterMenus = function(menus, destination, onlyVisible, enableDisableKeyStroke) {
return scout.menus.filterAccordingToSelection('Tree', this.selectedNodes.length, menus, destination, onlyVisible, enableDisableKeyStroke);
};
/**
* @override Widget.js
*/
scout.Tree.prototype._renderEnabled = function() {
scout.Tree.parent.prototype._renderEnabled.call(this);
var enabled = this.enabled;
this.$data.setEnabled(enabled);
this.$container.setTabbable(enabled);
if (this.rendered) {
// Enable/disable all checkboxes
this.$nodes().each(function() {
var $node = $(this),
node = $node.data('node');
$node.children('.tree-node-checkbox')
.children('.check-box')
.toggleClass('disabled', !(enabled && node.enabled));
});
}
};
/**
* @override Widget.js
*/
scout.Tree.prototype._renderDisabledStyle = function() {
scout.Tree.parent.prototype._renderDisabledStyle.call(this);
this._renderDisabledStyleInternal(this.$data);
};
scout.Tree.prototype.setCheckable = function(checkable) {
this.setProperty('checkable', checkable);
};
scout.Tree.prototype._setCheckable = function(checkable) {
this._setProperty('checkable', checkable);
if (this.checkable) {
this.nodePaddingLevel = this.nodePaddingLevelCheckable;
} else {
this.nodePaddingLevel = this.nodePaddingLevelNotCheckable;
}
};
scout.Tree.prototype.setCheckableStyle = function(checkableStyle) {
this.setProperty('checkableStyle', checkableStyle);
};
scout.Tree.prototype._renderCheckable = function() {
// Define helper functions
var isNodeRendered = function(node) {
return !!node.$node;
};
var updateCheckableStateRec = function(node) {
var $node = node.$node;
var $control = $node.children('.tree-node-control');
var $checkbox = $node.children('.tree-node-checkbox');
node._updateControl($control, this);
if (this.checkable) {
if ($checkbox.length === 0) {
node._renderCheckbox();
}
} else {
$checkbox.remove();
}
$node.css('padding-left', this._computeNodePaddingLeft(node));
// Recursion
if (node.childNodes) {
node.childNodes.filter(isNodeRendered).forEach(updateCheckableStateRec);
}
}.bind(this);
// Start recursion
this.nodes.filter(isNodeRendered).forEach(updateCheckableStateRec);
};
scout.Tree.prototype._renderDisplayStyle = function() {
this.$container.toggleClass('breadcrumb', this.isBreadcrumbStyleActive());
this._updateNodePaddingsLeft();
// update scrollbar if mode has changed (from tree to bc or vice versa)
this.invalidateLayoutTree();
};
scout.Tree.prototype._renderExpansion = function(node, options) {
var opts = {
expandLazyChanged: false,
expansionChanged: false
};
$.extend(opts, options);
var $node = node.$node,
expanded = node.expanded;
// Only render if node is rendered to make it possible to expand/collapse currently hidden nodes (used by collapseAll).
if (!$node || $node.length === 0) {
return;
}
// Only expand / collapse if there are child nodes
if (node.childNodes.length === 0) {
return true;
}
$node.toggleClass('lazy', expanded && node.expandedLazy);
if (!opts.expansionChanged && !opts.expandLazyChanged) {
// Expansion state has not changed -> return
return;
}
if (expanded) {
$node.addClass('expanded');
} else {
$node.removeClass('expanded');
}
};
scout.Tree.prototype._renderSelection = function() {
// Add children class to root nodes if no nodes are selected
if (this.selectedNodes.length === 0) {
this.nodes.forEach(function(childNode) {
if (childNode.rendered) {
childNode.$node.addClass('child-of-selected');
}
}, this);
}
this.selectedNodes.forEach(function(node) {
if (!this.visibleNodesMap[node.id]) {
return;
}
// Mark all ancestor nodes, especially necessary for bread crumb mode
var parentNode = node.parentNode;
if (parentNode && parentNode.rendered) {
parentNode.$node.addClass('parent-of-selected');
}
while (parentNode) {
if (parentNode.rendered) {
parentNode.$node.addClass('ancestor-of-selected');
}
parentNode = parentNode.parentNode;
}
// Mark all child nodes
if (node.expanded) {
node.childNodes.forEach(function(childNode) {
if (childNode.rendered) {
childNode.$node.addClass('child-of-selected');
}
}, this);
}
if (node.rendered) {
node.$node.select(true);
}
}, this);
// Update 'group' markers for all rendered nodes
for (var i = this.viewRangeRendered.from; i < this.viewRangeRendered.to; i++) {
if (i >= this.visibleNodesFlat.length) {
break;
}
var node = this.visibleNodesFlat[i];
if (node && node.rendered) {
node.$node.toggleClass('group', !!this.groupedNodes[node.id]);
}
}
this._updateNodePaddingsLeft();
this._highlightPrevSelectedNode();
if (this.scrollToSelection) {
this.revealSelection();
}
};
scout.Tree.prototype._renderCheckableStyle = function() {
this.$data.toggleClass('checkable', this.isTreeNodeCheckEnabled());
};
scout.Tree.prototype._highlightPrevSelectedNode = function() {
if (!this.isBreadcrumbStyleActive()) {
return;
}
if (!this.prevSelectedNode || !this.prevSelectedNode.rendered || this.prevSelectedNode.prevSelectionAnimationDone) {
return;
}
// Highlight previously selected node, but do it only once
this.prevSelectedNode.$node.addClassForAnimation('animate-prev-selected').oneAnimationEnd(function() {
this.prevSelectedNode.prevSelectionAnimationDone = true;
}.bind(this));
};
scout.Tree.prototype._removeSelection = function() {
// Remove children class on root nodes if no nodes were selected
if (this.selectedNodes.length === 0) {
this.nodes.forEach(function(childNode) {
if (childNode.rendered) {
childNode.$node.removeClass('child-of-selected');
}
}, this);
}
// Ensure animate-prev-selected class is removed (in case animation did not start)
if (this.prevSelectedNode && this.prevSelectedNode.rendered) {
this.prevSelectedNode.$node.removeClass('animate-prev-selected');
}
this.selectedNodes.forEach(this._removeNodeSelection, this);
};
scout.Tree.prototype._removeNodeSelection = function(node) {
if (node.rendered) {
node.$node.select(false);
}
// remove ancestor and child classes
var parentNode = node.parentNode;
if (parentNode && parentNode.rendered) {
parentNode.$node.removeClass('parent-of-selected');
}
while (parentNode && parentNode.rendered) {
parentNode.$node.removeClass('ancestor-of-selected');
parentNode = parentNode.parentNode;
}
if (node.expanded) {
node.childNodes.forEach(function(childNode) {
if (childNode.rendered) {
childNode.$node.removeClass('child-of-selected');
}
}, this);
}
};
scout.Tree.prototype._renderDropType = function() {
if (this.dropType) {
this._installDragAndDropHandler();
} else {
this._uninstallDragAndDropHandler();
}
};
scout.Tree.prototype._installDragAndDropHandler = function(event) {
if (this.dragAndDropHandler) {
return;
}
this.dragAndDropHandler = scout.dragAndDrop.handler(this, {
supportedScoutTypes: scout.dragAndDrop.SCOUT_TYPES.FILE_TRANSFER,
dropType: function() {
return this.dropType;
}.bind(this),
dropMaximumSize: function() {
return this.dropMaximumSize;
}.bind(this),
additionalDropProperties: function(event) {
var $target = $(event.currentTarget);
var properties = {
nodeId: ''
};
if ($target.hasClass('tree-node')) {
var node = $target.data('node');
properties.nodeId = node.id;
}
return properties;
}.bind(this)
});
this.dragAndDropHandler.install(this.$container, '.tree-data,.tree-node');
};
scout.Tree.prototype._uninstallDragAndDropHandler = function(event) {
if (!this.dragAndDropHandler) {
return;
}
this.dragAndDropHandler.uninstall();
this.dragAndDropHandler = null;
};
scout.Tree.prototype._updateMarkChildrenChecked = function(node, init, checked, checkChildrenChecked) {
if (!this.checkable) {
return;
}
if (checkChildrenChecked) {
var childrenFound = false;
for (var j = 0; j < node.childNodes.length > 0; j++) {
var childNode = node.childNodes[j];
if (childNode.checked || childNode.childrenChecked) {
node.childrenChecked = true;
checked = true;
childrenFound = true;
if (this.rendered && node.$node) {
node.$node
.children('.tree-node-checkbox')
.children('.check-box')
.toggleClass('children-checked', true);
}
break;
}
}
if (!childrenFound) {
node.childrenChecked = false;
if (this.rendered && node.$node) {
node.$node.children('.tree-node-checkbox')
.children('.check-box')
.toggleClass('children-checked', false);
}
}
}
if (!node.parentNode || node.parentNode.checked) {
return;
}
var stateChanged = false;
if (!checked && !init) {
//node was unchecked check siblings
var hasCheckedSiblings = false;
for (var i = 0; i < node.parentNode.childNodes.length > 0; i++) {
var siblingNode = node.parentNode.childNodes[i];
if (siblingNode.checked || siblingNode.childrenChecked) {
hasCheckedSiblings = true;
break;
}
}
if (hasCheckedSiblings !== node.parentNode.childrenChecked) {
//parentNode.checked should be false
node.parentNode.childrenChecked = hasCheckedSiblings;
stateChanged = true;
}
}
if ((checked && !node.parentNode.childrenChecked)) {
node.parentNode.childrenChecked = true;
stateChanged = true;
}
if (stateChanged) {
this._updateMarkChildrenChecked(node.parentNode, init, checked);
if (this.rendered && node.parentNode.$node) {
if (checked) {
node.parentNode.$node.children('.tree-node-checkbox')
.children('.check-box')
.toggleClass('children-checked', true);
} else {
node.parentNode.$node.children('.tree-node-checkbox')
.children('.check-box')
.toggleClass('children-checked', false);
}
}
}
};
scout.Tree.prototype._installNodeTooltipSupport = function() {
scout.tooltips.install(this.$data, {
parent: this,
selector: '.tree-node',
text: this._nodeTooltipText.bind(this),
arrowPosition: 50,
arrowPositionUnit: '%',
nativeTooltip: !scout.device.isCustomEllipsisTooltipPossible()
});
};
scout.Tree.prototype._uninstallNodeTooltipSupport = function() {
scout.tooltips.uninstall(this.$data);
};
scout.Tree.prototype._nodeTooltipText = function($node) {
var node = $node.data('node');
if (node.tooltipText) {
return node.tooltipText;
} else if (this._isTruncatedNodeTooltipEnabled() && $node.isContentTruncated()) {
return node.$text.text();
}
};
scout.Tree.prototype._isTruncatedNodeTooltipEnabled = function() {
return true;
};
scout.Tree.prototype.setDisplayStyle = function(displayStyle) {
if (this.displayStyle === displayStyle) {
return;
}
this._renderViewportBlocked = true;
this._setDisplayStyle(displayStyle);
if (this.rendered) {
this._renderDisplayStyle();
}
this._renderViewportBlocked = false;
};
scout.Tree.prototype._setDisplayStyle = function(displayStyle) {
this._setProperty('displayStyle', displayStyle);
if (this.displayStyle === scout.Tree.DisplayStyle.BREADCRUMB) {
if (this.selectedNodes.length > 0) {
var selectedNode = this.selectedNodes[0];
if (!selectedNode.expanded) {
this.expandNode(selectedNode);
}
}
this.addFilter(this.breadcrumbFilter, true, true);
this.filterVisibleNodes();
} else {
this.removeFilter(this.breadcrumbFilter, true);
this.filter();
}
};
scout.Tree.prototype._updateNodePaddingsLeft = function() {
this.$nodes().each(function(index, element) {
var $node = $(element),
node = $node.data('node'),
paddingLeft = this._computeNodePaddingLeft(node);
$node.css('padding-left', scout.objects.isNullOrUndefined(paddingLeft) ? '' : paddingLeft);
}.bind(this));
};
scout.Tree.prototype.setBreadcrumbStyleActive = function(active) {
if (active) {
this.setDisplayStyle(scout.Tree.DisplayStyle.BREADCRUMB);
} else {
this.setDisplayStyle(scout.Tree.DisplayStyle.DEFAULT);
}
};
scout.Tree.prototype.isNodeInBreadcrumbVisible = function(node) {
return this._inSelectionPathList[node.id] === undefined ? false : this._inSelectionPathList[node.id];
};
scout.Tree.prototype.isBreadcrumbStyleActive = function() {
return this.displayStyle === scout.Tree.DisplayStyle.BREADCRUMB;
};
scout.Tree.prototype.setBreadcrumbTogglingThreshold = function(width) {
this.setProperty('breadcrumbTogglingThreshold', width);
};
scout.Tree.prototype.expandNode = function(node, opts) {
this.setNodeExpanded(node, true, opts);
};
scout.Tree.prototype.collapseNode = function(node, opts) {
this.setNodeExpanded(node, false, opts);
};
scout.Tree.prototype.collapseAll = function() {
this.rebuildSuppressed = true;
// Collapse all expanded child nodes (only model)
this.visitNodes(function(node) {
this.collapseNode(node);
}.bind(this));
if (this.rendered) {
// ensure correct rendering
this._rerenderViewport();
}
this.rebuildSuppressed = false;
};
scout.Tree.prototype.setNodeExpanded = function(node, expanded, opts) {
opts = opts || {};
var lazy = scout.nvl(opts.lazy, node.lazyExpandingEnabled);
var renderAnimated = scout.nvl(opts.renderAnimated, true);
// Never do lazy expansion if it is disabled on the tree
if (!this.lazyExpandingEnabled) {
lazy = false;
}
if (this.isBreadcrumbStyleActive()) {
// Do not allow to collapse a selected node
if (!expanded && this.selectedNodes.indexOf(node) > -1) {
this.setNodeExpanded(node, true, opts);
return;
}
}
// Do not allow a lazy expansion/collapse if the node was manually expanded
if (lazy && node.expanded && !node.expandedLazy) {
return;
}
// Optionally collapse all children (recursively)
if (opts.collapseChildNodes) {
// Suppress render expansion
var childOpts = scout.objects.valueCopy(opts);
childOpts.renderExpansion = false;
node.childNodes.forEach(function(childNode) {
if (childNode.expanded) {
this.collapseNode(childNode, childOpts);
}
}.bind(this));
}
var renderExpansionOpts = {
expansionChanged: false,
expandLazyChanged: false
};
// Set expansion state
if (node.expanded !== expanded || node.expandedLazy !== lazy) {
renderExpansionOpts.expansionChanged = node.expanded !== expanded;
renderExpansionOpts.expandLazyChanged = node.expandedLazy !== lazy;
node.expanded = expanded;
node.expandedLazy = lazy;
var filterStateChanged = this._applyFiltersForNode(node);
if (filterStateChanged && renderExpansionOpts.expansionChanged) {
if (node.parentNode) {
// ensure node is visible under the parent node if there is a parent.
this._rebuildParent(node.parentNode, opts);
} else {
if (node.filterAccepted) {
this._addToVisibleFlatList(node, false);
} else {
this._removeFromFlatList(node, false);
}
}
} else if (renderExpansionOpts.expandLazyChanged) {
node.childNodes.forEach(function(child) {
this._applyFiltersForNode(child);
}.bind(this));
}
if (this.groupedNodes[node.id]) {
this._updateItemPath(false, node);
}
if (node.expanded) {
node.ensureLoadChildren().done(
this._addChildrenToFlatList.bind(this, node, null, renderAnimated, null, true));
} else {
this._removeChildrenFromFlatList(node, renderAnimated);
}
this.trigger('nodeExpanded', {
node: node,
expanded: expanded,
expandedLazy: lazy
});
this.viewRangeDirty = true;
}
// Render expansion
if (this.rendered && scout.nvl(opts.renderExpansion, true)) {
this._renderExpansion(node, renderExpansionOpts);
}
if (this.rendered) {
this.ensureExpansionVisible(node);
}
};
scout.Tree.prototype.setNodeExpandedRecursive = function(nodes, expanded, opts) {
scout.Tree.visitNodes(function(childNode) {
this.setNodeExpanded(childNode, expanded, opts);
}.bind(this), nodes);
};
scout.Tree.prototype._rebuildParent = function(node, opts) {
if (this.rebuildSuppressed) {
return;
}
if (node.expanded || node.expandedLazy) {
this._addChildrenToFlatList(node, null, false, null, true);
} else {
this._removeChildrenFromFlatList(node, false);
}
// Render expansion
if (this.rendered && scout.nvl(opts.renderExpansion, true)) {
var renderExpansionOpts = {
expansionChanged: true
};
this._renderExpansion(node, renderExpansionOpts);
}
};
scout.Tree.prototype._removeChildrenFromFlatList = function(parentNode, animatedRemove) {
// Only if a parent is available the children are available.
if (this.visibleNodesMap[parentNode.id]) {
var parentIndex = this.visibleNodesFlat.indexOf(parentNode);
var elementsToDelete = 0;
var parentLevel = parentNode.level;
var removedNodes = [];
animatedRemove = animatedRemove && this.rendered;
if (this._$animationWrapper) {
// Note: Do _not_ use finish() here! Although documentation states that it is "similar" to stop(true, true),
// this does not seem to be the case. Implementations differ slightly in details. The effect is, that when
// calling stop() the animation stops and the 'complete' callback is executed immediately. However, when calling
// finish(), the callback is _not_ executed! (This may or may not be a bug in jQuery, I cannot tell...)
this._$animationWrapper.stop(false, true);
}
this._$expandAnimationWrappers.forEach(function($wrapper) {
$wrapper.stop(false, true);
});
for (var i = parentIndex + 1; i < this.visibleNodesFlat.length; i++) {
if (this.visibleNodesFlat[i].level > parentLevel) {
var node = this.visibleNodesFlat[i];
if (this.isHorizontalScrollingEnabled()) {
//if node is the node which defines the widest width then recalculate width for render
if (node.width === this.maxNodeWidth) {
this.maxNodeWidth = 0;
this.nodeWidthDirty = true;
}
}
delete this.visibleNodesMap[this.visibleNodesFlat[i].id];
if (node.attached && animatedRemove) {
if (!this._$animationWrapper) {
this._$animationWrapper = $('<div class="animation-wrapper">').insertBefore(node.$node);
this._$animationWrapper.data('parentNode', parentNode);
}
if (node.isChildOf(this._$animationWrapper.data('parentNode'))) {
this._$animationWrapper.append(node.$node);
}
node.attached = false;
node.displayBackup = node.$node.css('display');
removedNodes.push(node);
} else if (node.attached && !animatedRemove) {
this.hideNode(node, false, false);
}
elementsToDelete++;
} else {
break;
}
}
this.visibleNodesFlat.splice(parentIndex + 1, elementsToDelete);
// animate closing
if (animatedRemove) { // don't animate while rendering (not necessary, or may even lead to timing issues)
this._renderViewportBlocked = true;
if (removedNodes.length > 0) {
this._$animationWrapper.animate({
height: 0
}, {
start: this.startAnimationFunc,
complete: onAnimationComplete.bind(this, removedNodes),
step: this.revalidateLayoutTree.bind(this),
duration: 200,
queue: false
});
} else if (this._$animationWrapper) {
this._$animationWrapper.remove();
this._$animationWrapper = null;
onAnimationComplete.call(this, removedNodes);
} else {
this._renderViewportBlocked = false;
}
}
return removedNodes;
}
//----- Helper functions -----
function onAnimationComplete(affectedNodes) {
affectedNodes.forEach(function(node) {
node.$node.detach();
node.$node.css('display', node.displayBackup);
node.displayBackup = null;
});
if (this._$animationWrapper) {
this._$animationWrapper.remove();
this._$animationWrapper = null;
}
this.runningAnimationsFinishFunc();
}
};
scout.Tree.prototype._removeFromFlatList = function(node, animatedRemove) {
var removedNodes = [];
if (this.visibleNodesMap[node.id]) {
var index = this.visibleNodesFlat.indexOf(node);
this._removeChildrenFromFlatList(node, false);
if (this.isHorizontalScrollingEnabled()) {
//if node is the node which defines the widest width then recalculate width for render
if (node.width === this.maxNodeWidth) {
this.maxNodeWidth = 0;
this.nodeWidthDirty = true;
}
}
removedNodes = scout.arrays.ensure(this.visibleNodesFlat.splice(index, 1));
delete this.visibleNodesMap[node.id];
this.hideNode(node, animatedRemove);
}
removedNodes.push(node);
return removedNodes;
};
/**
* @returns {boolean} whether or not the function added a node to the flat list
*/
scout.Tree.prototype._addToVisibleFlatList = function(node, renderingAnimated) {
// if node already is in visible list don't do anything. If no parentNode is available this node is on toplevel, if a parent is available
// it has to be in visible list and also be expanded
if (!this.visibleNodesMap[node.id] && node.isFilterAccepted() && (!node.parentNode ||
(node.parentNode.expanded && this.visibleNodesMap[node.parentNode.id]))) {
if (this.initialTraversing) {
// for faster index calculation
this._addToVisibleFlatListNoCheck(node, this.visibleNodesFlat.length, renderingAnimated);
} else {
var insertIndex = this._findInsertPositionInFlatList(node);
this._addToVisibleFlatListNoCheck(node, insertIndex, renderingAnimated);
}
}
};
// TODO [7.0] CGU applies to all the add/remove to/from flat list methods:
// Is it really necessary to update dom on every operation? why not just update the list and renderViewport at the end?
// The update of the flat list is currently implemented quite complicated -> it should be simplified.
// And: because add to flat list renders all the children the rendered node count is greater than the viewRangeSize until
// the layout renders the viewport again -> this must not happen (can be seen when a node gets expanded)
scout.Tree.prototype._addChildrenToFlatList = function(parentNode, parentIndex, animatedRendering, insertBatch, forceFilter) {
// add nodes recursively
if (!this.visibleNodesMap[parentNode.id]) {
return 0;
}
var isSubAdding = !!insertBatch;
parentIndex = parentIndex ? parentIndex : this.visibleNodesFlat.indexOf(parentNode);
animatedRendering = animatedRendering && this.rendered; // don't animate while rendering (not necessary, or may even lead to timing issues)
if (this._$animationWrapper && !isSubAdding) {
// Note: Do _not_ use finish() here! Although documentation states that it is "similar" to stop(true, true),
// this does not seem to be the case. Implementations differ slightly in details. The effect is, that when
// calling stop() the animation stops and the 'complete' callback is executed immediately. However, when calling
// finish(), the callback is _not_ executed! (This may or may not be a bug in jQuery, I cannot tell...)
this._$animationWrapper.stop(false, true);
}
if (insertBatch) {
insertBatch.setInsertAt(parentIndex);
} else {
insertBatch = this.newInsertBatch(parentIndex + 1);
}
parentNode.childNodes.forEach(function(node, index) {
if (!node.initialized || !node.isFilterAccepted(forceFilter)) {
return;
}
var insertIndex, isAlreadyAdded = this.visibleNodesMap[node.id];
if (isAlreadyAdded) {
this.insertBatchInVisibleNodes(insertBatch, this._showNodes(insertBatch), animatedRendering);
this.checkAndHandleBatchAnimationWrapper(parentNode, animatedRendering, insertBatch);
insertBatch = this.newInsertBatch(insertBatch.nextBatchInsertIndex());
insertBatch = this._addChildrenToFlatListIfExpanded(1, node, insertIndex, animatedRendering, insertBatch, forceFilter);
// do not animate following
animatedRendering = false;
} else {
insertBatch.insertNodes.push(node);
this.visibleNodesMap[node.id] = true;
insertBatch = this.checkAndHandleBatch(insertBatch, parentNode, animatedRendering);
insertBatch = this._addChildrenToFlatListIfExpanded(0, node, insertIndex, animatedRendering, insertBatch, forceFilter);
}
}.bind(this));
if (!isSubAdding) {
// animation is not done yet and all added nodes are in visible range
this.insertBatchInVisibleNodes(insertBatch, this._showNodes(insertBatch), animatedRendering);
this.invalidateLayoutTree();
}
return insertBatch;
};
/**
* Checks if the given node is expanded, and if that's the case determine the insert index of the node and add its children to the flat list.
*
* @param {number} indexOffset either 0 or 1, offset is added to the insert index
*/
scout.Tree.prototype._addChildrenToFlatListIfExpanded = function(indexOffset, node, insertIndex, animatedRendering, insertBatch, forceFilter) {
if (node.expanded && node.childNodes.length) {
if (insertBatch.containsNode(node.parentNode) || insertBatch.length() > 1) {
// if parent node is already in the batch, do not change the insertIndex,
// only append child nodes below that parent node
// Also, if the batch is not empty (i.e. contains more nodes than the current node),
// the insert index was already calculated previously and must not be changed.
insertIndex = insertBatch.insertAt();
} else {
insertIndex = this._findInsertPositionInFlatList(node);
}
insertIndex += indexOffset;
insertBatch = this._addChildrenToFlatList(node, insertIndex, animatedRendering, insertBatch, forceFilter);
}
return insertBatch;
};
scout.Tree.prototype._showNodes = function(insertBatch) {
return this.viewRangeRendered.from + this.viewRangeSize >= insertBatch.lastBatchInsertIndex() &&
this.viewRangeRendered.from <= insertBatch.lastBatchInsertIndex();
};
/**
* This function tries to find the correct insert position within the flat list for the given node.
* The function must consider the order of child nodes in the original tree structure and then check
* where in the flat list this position is.
*/
scout.Tree.prototype._findInsertPositionInFlatList = function(node) {
var childNodes,
parentNode = node.parentNode;
// use root nodes as nodes when no other parent node is available (root case)
if (parentNode) {
childNodes = parentNode.childNodes;
} else {
childNodes = this.nodes;
}
// find all visible siblings for our node (incl. our own node, which is probably not yet
// in the visible nodes map)
var thatNode = node;
var siblings = childNodes.filter(function(node) {
return !!this.visibleNodesMap[node.id] || node === thatNode;
}.bind(this));
// when there are no visible siblings, insert below the parent node
if (siblings.length === 0) {
return this._findPositionInFlatList(parentNode) + 1;
}
var nodePos = siblings.indexOf(node);
// when there are no prev. siblings in the flat list, insert below the parent node
if (nodePos === 0) {
return this._findPositionInFlatList(parentNode) + 1;
}
var prevSiblingNode = siblings[nodePos - 1];
var prevSiblingPos = this._findPositionInFlatList(prevSiblingNode);
// when the prev. sibling is not in the flat list, insert below the parent node
if (prevSiblingPos === -1) {
return this._findPositionInFlatList(parentNode) + 1;
}
// find the index of the last child element of our prev. sibling node
// that's where we want to insert the new node. We go down the flat list
// starting from the prev. sibling node, until we hit a node that does not
// belong to the sub tree of the prev. sibling node.
var i, checkNode;
for (i = prevSiblingPos; i < this.visibleNodesFlat.length; i++) {
checkNode = this.visibleNodesFlat[i];
if (!this._isInSameSubTree(prevSiblingNode, checkNode)) {
return i;
}
}
// insert at the end of the list
return this.visibleNodesFlat.length;
};
scout.Tree.prototype._findPositionInFlatList = function(node) {
return this.visibleNodesFlat.indexOf(node);
};
/**
* Checks whether the given checkNode belongs to the same sub tree (or is) the given node.
* The function goes up all parentNodes of the checkNode.
*
* @param {scout.TreeNode} node which is used to for the sub tree comparison
* @param {scout.TreeNode} checkNode node which is checked against the given node
* @returns {boolean}
*/
scout.Tree.prototype._isInSameSubTree = function(node, checkNode) {
do {
if (checkNode === node || checkNode.parentNode === node) {
return true;
}
checkNode = checkNode.parentNode;
} while (checkNode);
return false;
};
/**
* Returns true if the given node is a child of one of the selected nodes.
* The functions goes up the parent node hierarchy.
*
* @param {scout.TreeNode} node to check
* @returns {boolean}
*/
scout.Tree.prototype._isChildOfSelectedNodes = function(node) {
while (node) {
if (this.selectedNodes.indexOf(node.parentNode) > -1) {
return true;
}
node = node.parentNode;
}
return false;
};
/**
* Info: the object created here is a bit weird: the array 'insertNodes' is used as function arguments to the Array#splice function at some point.
* The signature of that function is: array.splice(index, deleteCount[, element1[, element2 [, ...]]])
* So the first two elements are numbers and all the following elements are TreeNodes or Pages.
*/
scout.Tree.prototype.newInsertBatch = function(insertIndex) {
return {
insertNodes: [insertIndex, 0], // second element is always 0 (used as argument for deleteCount in Array#splice)
$animationWrapper: null,
lastBatchInsertIndex: function() {
if (this.isEmpty()) {
return this.insertAt();
} else {
return this.insertAt() + this.insertNodes.length - 3;
}
},
nextBatchInsertIndex: function() {
// only NBU knows what this means
return this.lastBatchInsertIndex() + (this.isEmpty() ? 1 : 2);
},
isEmpty: function() {
return this.insertNodes.length === 2;
},
length: function() {
return this.insertNodes.length - 2;
},
insertAt: function() {
return this.insertNodes[0];
},
setInsertAt: function(insertAt) {
this.insertNodes[0] = insertAt;
},
containsNode: function(node) {
return this.insertNodes.indexOf(node) !== -1;
}
};
};
scout.Tree.prototype.checkAndHandleBatchAnimationWrapper = function(parentNode, animatedRendering, insertBatch) {
if (animatedRendering && this.viewRangeRendered.from <= insertBatch.lastBatchInsertIndex() && this.viewRangeRendered.to >= insertBatch.lastBatchInsertIndex() && !insertBatch.$animationWrapper) {
//we are in visible area so we need a animation wrapper
//if parent is in visible area insert after parent else insert before first node.
var lastNodeIndex = insertBatch.lastBatchInsertIndex() - 1,
nodeBefore = this.viewRangeRendered.from === insertBatch.lastBatchInsertIndex() ? null : this.visibleNodesFlat[lastNodeIndex];
if (nodeBefore && lastNodeIndex >= this.viewRangeRendered.from && lastNodeIndex < this.viewRangeRendered.to && !nodeBefore.attached) {
//ensure node before is visible
this.showNode(nodeBefore, false, lastNodeIndex);
}
if (nodeBefore && nodeBefore.attached) {
insertBatch.$animationWrapper = $('<div class="animation-wrapper">').insertAfter(nodeBefore.$node);
} else if (parentNode.attached) {
insertBatch.$animationWrapper = $('<div class="animation-wrapper">').insertAfter(parentNode.$node);
} else if (this.$fillBefore) {
insertBatch.$animationWrapper = $('<div class="animation-wrapper">').insertAfter(this.$fillBefore);
} else {
var nodeAfter = this.visibleNodesFlat[insertBatch.lastBatchInsertIndex()];
insertBatch.$animationWrapper = $('<div class="animation-wrapper">').insertBefore(nodeAfter.$node);
}
insertBatch.animationCompleteFunc = onAnimationComplete;
this._$expandAnimationWrappers.push(insertBatch.$animationWrapper);
}
//----- Helper functions ----- //
function onAnimationComplete() {
insertBatch.$animationWrapper.replaceWith(insertBatch.$animationWrapper.contents());
scout.arrays.remove(this._$expandAnimationWrappers, insertBatch.$animationWrapper);
insertBatch.$animationWrapper = null;
this.runningAnimationsFinishFunc();
}
};
scout.Tree.prototype.checkAndHandleBatch = function(insertBatch, parentNode, animatedRendering) {
if (this.viewRangeRendered.from - 1 === insertBatch.lastBatchInsertIndex()) {
//do immediate rendering because list could be longer
this.insertBatchInVisibleNodes(insertBatch, false, false);
insertBatch = this.newInsertBatch(insertBatch.lastBatchInsertIndex() + 1);
}
this.checkAndHandleBatchAnimationWrapper(parentNode, animatedRendering, insertBatch);
if (this.viewRangeRendered.from + this.viewRangeSize - 1 === insertBatch.lastBatchInsertIndex()) {
//do immediate rendering because list could be longer
this.insertBatchInVisibleNodes(insertBatch, true, animatedRendering);
insertBatch = this.newInsertBatch(insertBatch.lastBatchInsertIndex() + 1);
}
return insertBatch;
};
scout.Tree.prototype.insertBatchInVisibleNodes = function(insertBatch, showNodes, animate) {
if (insertBatch.isEmpty()) {
// nothing to add
return;
}
this.visibleNodesFlat.splice.apply(this.visibleNodesFlat, insertBatch.insertNodes);
if (showNodes) {
var indexHint = insertBatch.insertAt();
for (var i = 2; i < insertBatch.insertNodes.length; i++) {
var node = insertBatch.insertNodes[i];
this.showNode(node, false, indexHint);
if (insertBatch.$animationWrapper) {
insertBatch.$animationWrapper.append(node.$node);
}
indexHint++;
}
if (insertBatch.$animationWrapper) {
var h = insertBatch.$animationWrapper.outerHeight();
insertBatch.$animationWrapper
.css('height', 0)
.animate({
height: h
}, {
start: this.startAnimationFunc,
complete: insertBatch.animationCompleteFunc.bind(this),
step: this.revalidateLayoutTree.bind(this),
duration: 200,
queue: false
});
}
} else if (insertBatch.$animationWrapper && insertBatch.animationCompleteFunc) {
insertBatch.animationCompleteFunc.call(this);
}
};
scout.Tree.prototype._addToVisibleFlatListNoCheck = function(node, insertIndex, animatedRendering) {
scout.arrays.insert(this.visibleNodesFlat, node, insertIndex);
this.visibleNodesMap[node.id] = true;
if (this.rendered) {
this.showNode(node, animatedRendering, insertIndex);
}
};
scout.Tree.prototype.scrollTo = function(node, options) {
if (this.viewRangeRendered.size() === 0) {
// Cannot scroll to a node if no node is rendered
return;
}
if (!node.attached) {
this._renderViewRangeForNode(node);
}
scout.scrollbars.scrollTo(this.$data, node.$node, options);
};
scout.Tree.prototype.revealSelection = function() {
if (!this.rendered) {
// Execute delayed because tree may be not layouted yet
this.session.layoutValidator.schedulePostValidateFunction(this.revealSelection.bind(this));
return;
}
if (this.selectedNodes.length > 0) {
if (!this.visibleNodesMap[this.selectedNodes[0].id]) {
this._expandAllParentNodes(this.selectedNodes[0]);
}
this.scrollTo(this.selectedNodes[0]);
this.ensureExpansionVisible(this.selectedNodes[0]);
}
};
scout.Tree.prototype.ensureExpansionVisible = function(node) {
// only scroll if treenode is in dom and the current node is selected (user triggered expansion change)
if (!node || !node.$node || this.selectedNodes[0] !== node) {
return;
}
scout.scrollbars.ensureExpansionVisible({
element: node,
$element: node.$node,
$scrollable: this.get$Scrollable(),
isExpanded: function(element) {
return element.expanded;
},
getChildren: function(parent) {
return parent.childNodes;
},
nodePaddingLevel: this.nodePaddingLevel,
defaultChildHeight: this.nodeHeight
});
};
scout.Tree.prototype.deselectAll = function() {
this.selectNodes([]);
};
scout.Tree.prototype.selectNode = function(node, debounceSend) {
this.selectNodes(node, debounceSend);
};
scout.Tree.prototype.selectNodes = function(nodes, debounceSend) {
nodes = scout.arrays.ensure(nodes);
// TODO [8.0] CGU Actually, the nodes should be filtered here so that invisible nodes may not be selected
// But this is currently not possible because the LazyNodeFilter would not accept the nodes
// We would have to keep track of the clicked nodes and check them in the lazy node filter (e.g. selectedNode.parentNode.lazySelectedChildNodes[selectedNode.id] = selectedNode).
// But since this requires a change in setNodeExpanded as well we decided to not implement it until the TODO at _addChildrenToFlatList is solved
if (scout.arrays.equalsIgnoreOrder(nodes, this.selectedNodes)) {
return;
}
if (this.rendered) {
this._rememberScrollTopBeforeSelection();
this._removeSelection();
}
if (this.prevSelectedNode) {
this.prevSelectedNode.prevSelectionAnimationDone = false;
}
this.prevSelectedNode = this.selectedNodes[0];
this._setSelectedNodes(nodes, debounceSend);
if (this.rendered) {
this._renderSelection();
this._updateScrollTopAfterSelection();
}
};
scout.Tree.prototype._rememberScrollTopBeforeSelection = function() {
if (this.isBreadcrumbStyleActive()) {
// Save the current scrollTop for future up navigation
if (this.selectedNodes.length > 0) {
this.scrollTopHistory[this.selectedNodes[0].level] = this.$data[0].scrollTop;
}
} else {
// Clear history if user now works with tree to not get confused when returning to bc mode
this.scrollTopHistory = [];
}
};
scout.Tree.prototype._updateScrollTopAfterSelection = function() {
if (!this.isBreadcrumbStyleActive()) {
return;
}
var currentLevel = -1;
if (this.selectedNodes.length > 0) {
currentLevel = this.selectedNodes[0].level;
}
// Remove positions after the current level (no restore when going down, only when going up)
this.scrollTopHistory.splice(currentLevel + 1);
// Read the scroll top for the current level and use that one if it is set
var scrollTopForLevel = this.scrollTopHistory[currentLevel];
if (scrollTopForLevel >= 0) {
this.setScrollTop(scrollTopForLevel);
}
};
scout.Tree.prototype._setSelectedNodes = function(nodes, debounceSend) {
// Make a copy so that original array stays untouched
this.selectedNodes = nodes.slice();
this._nodesSelectedInternal();
this._triggerNodesSelected(debounceSend);
if (this.selectedNodes.length > 0 && !this.visibleNodesMap[this.selectedNodes[0].id]) {
this._expandAllParentNodes(this.selectedNodes[0]);
}
this._updateItemPath(true);
if (this.isBreadcrumbStyleActive()) {
// In breadcrumb mode selected node has to expanded
if (this.selectedNodes.length > 0 && !this.selectedNodes[0].expanded) {
this.expandNode(this.selectedNodes[0]);
this.selectedNodes[0].filterDirty = true;
}
this.filter(true);
}
this.session.onRequestsDone(this._updateMenuBar.bind(this));
};
/**
* This method is overridden by subclasses of Tree. The default impl. does nothing.
*/
scout.Tree.prototype._nodesSelectedInternal = function(node) {
// NOP
};
scout.Tree.prototype.deselectNode = function(node) {
this.deselectNodes(node);
};
scout.Tree.prototype.deselectNodes = function(nodes) {
nodes = scout.arrays.ensure(nodes);
var selectedNodes = this.selectedNodes.slice(); // copy
if (scout.arrays.removeAll(selectedNodes, nodes)) {
this.selectNodes(selectedNodes);
}
};
scout.Tree.prototype.isNodeSelected = function(node) {
return this.selectedNodes.indexOf(node) > -1;
};
scout.Tree.prototype._computeNodePaddingLeft = function(node) {
this._computeNodePaddings();
if (this.isBreadcrumbStyleActive()) {
return null;
}
var padding = node.level * this.nodePaddingLevel + this.nodePaddingLeft;
if (this.checkable) {
padding += this.nodeCheckBoxPaddingLeft;
}
return padding;
};
/**
* Reads the paddings from CSS and stores them in nodePaddingLeft and nodeControlPaddingLeft
*/
scout.Tree.prototype._computeNodePaddings = function() {
if (this.nodePaddingLeft !== null && this.nodeControlPaddingLeft !== null) {
return;
}
var $dummyNode = this.$data.appendDiv('tree-node');
var $dummyNodeControl = $dummyNode.appendDiv('tree-node-control');
if (this.nodePaddingLeft === null) {
this.nodePaddingLeft = $dummyNode.cssPaddingLeft();
}
if (this.nodeControlPaddingLeft === null) {
this.nodeControlPaddingLeft = $dummyNodeControl.cssPaddingLeft();
}
$dummyNode.remove();
};
scout.Tree.prototype._expandAllParentNodes = function(node) {
var i, currNode = node,
parentNodes = [];
currNode = node;
var nodesToInsert = [];
while (currNode.parentNode) {
parentNodes.push(currNode.parentNode);
if (!this.visibleNodesMap[currNode.id]) {
nodesToInsert.push(currNode);
}
currNode = currNode.parentNode;
}
for (i = parentNodes.length - 1; i >= 0; i--) {
if (nodesToInsert.indexOf(parentNodes[i]) !== -1) {
this._addToVisibleFlatList(parentNodes[i], false);
}
if (!parentNodes[i].expanded) {
this.expandNode(parentNodes[i], {
renderExpansion: false,
renderAnimated: false
});
}
}
if (this.rendered && nodesToInsert.length > 0) {
this._rerenderViewport();
this.invalidateLayoutTree();
}
};
scout.Tree.prototype._updateChildNodeIndex = function(nodes, startIndex) {
for (var i = scout.nvl(startIndex, 0); i < nodes.length; i++) {
nodes[i].childNodeIndex = i;
}
};
scout.Tree.prototype.insertNode = function(node, parentNode) {
this.insertNodes([node], parentNode);
};
scout.Tree.prototype.insertNodes = function(nodes, parentNode) {
nodes = scout.arrays.ensure(nodes).slice();
this._ensureTreeNodes(nodes);
if (parentNode && !(parentNode instanceof scout.TreeNode)) {
throw new Error('parent has to be a tree node: ' + parentNode);
}
// Append continuous node blocks
nodes.sort(function(a, b) {
return a.childNodeIndex - b.childNodeIndex;
});
// Update parent with new child nodes
if (parentNode) {
if (parentNode.childNodes && parentNode.childNodes.length > 0) {
nodes.forEach(function(entry) {
// only insert node if not already existing
if (parentNode.childNodes.indexOf(entry) < 0) {
scout.arrays.insert(parentNode.childNodes, entry, entry.childNodeIndex);
}
}.bind(this));
this._updateChildNodeIndex(parentNode.childNodes, nodes[0].childNodeIndex);
} else {
nodes.forEach(function(entry) {
parentNode.childNodes.push(entry);
}.bind(this));
}
//initialize node and add to visible list if node is visible
scout.Tree.visitNodes(this._initTreeNode.bind(this), nodes, parentNode);
scout.Tree.visitNodes(this._updateFlatListAndSelectionPath.bind(this), nodes, parentNode);
if (this.groupedNodes[parentNode.id]) {
this._updateItemPath(false, parentNode);
}
if (this.rendered) {
var opts = {
expansionChanged: true
};
this._renderExpansion(parentNode, opts);
this.ensureExpansionVisible(parentNode);
}
} else {
if (this.nodes && this.nodes.length > 0) {
nodes.forEach(function(entry) {
// only insert node if not already existing
if (this.nodes.indexOf(entry) < 0) {
scout.arrays.insert(this.nodes, entry, entry.childNodeIndex);
}
}.bind(this));
this._updateChildNodeIndex(this.nodes, nodes[0].childNodeIndex);
} else {
scout.arrays.pushAll(this.nodes, nodes);
}
//initialize node and add to visible list if node is visible
scout.Tree.visitNodes(this._initTreeNode.bind(this), nodes, parentNode);
scout.Tree.visitNodes(this._updateFlatListAndSelectionPath.bind(this), nodes, parentNode);
}
if (this.rendered) {
this.viewRangeDirty = true;
this.invalidateLayoutTree();
}
this.trigger('nodesInserted', {
nodes: nodes,
parentNode: parentNode
});
};
scout.Tree.prototype.updateNode = function(node) {
this.updateNodes([node]);
};
scout.Tree.prototype.updateNodes = function(nodes) {
nodes = scout.arrays.ensure(nodes);
nodes.forEach(function(updatedNode) {
var propertiesChanged,
oldNode = this.nodesMap[updatedNode.id];
// if same instance has been updated we must set the flag always to true
// because we cannot compare against an "old" node
if (updatedNode === oldNode) {
propertiesChanged = true;
} else {
this._applyNodeDefaultValues(updatedNode);
propertiesChanged = this._applyUpdatedNodeProperties(oldNode, updatedNode);
}
if (propertiesChanged) {
if (this._applyFiltersForNode(oldNode)) {
if (!oldNode.isFilterAccepted()) {
this._nodesFiltered([oldNode]);
this._removeFromFlatList(oldNode, false);
} else {
this._addToVisibleFlatList(oldNode, false);
}
}
this._updateItemPath(false, oldNode.parentNode);
if (this.rendered) {
oldNode._decorate();
}
}
}, this);
this.trigger('nodesUpdated', {
nodes: nodes
});
};
/**
* Called by _onNodesUpdated for every updated node. The function is expected to apply
* all updated properties from the updatedNode to the oldNode. May be overridden by
* subclasses so update their specific node properties.
*
* @param oldNode
* The target node to be updated
* @param updatedNode
* The new node with potentially updated properties. Default values are already applied!
* @returns
* true if at least one property has changed, false otherwise. This value is used to
* determine if the node has to be rendered again.
*/
scout.Tree.prototype._applyUpdatedNodeProperties = function(oldNode, updatedNode) {
// Note: We only update _some_ of the properties, because everything else will be handled
// with separate events. --> See also: JsonTree.java/handleModelNodesUpdated()
var propertiesChanged = false;
if (oldNode.leaf !== updatedNode.leaf) {
oldNode.leaf = updatedNode.leaf;
propertiesChanged = true;
}
if (oldNode.enabled !== updatedNode.enabled) {
oldNode.enabled = updatedNode.enabled;
propertiesChanged = true;
}
if (oldNode.lazyExpandingEnabled !== updatedNode.lazyExpandingEnabled) {
oldNode.lazyExpandingEnabled = updatedNode.lazyExpandingEnabled;
// Also make sure expandedLazy is reset to false when lazyExpanding is disabled (same code as in AbstractTreeNode.setLazyExpandingEnabled)
if (!updatedNode.lazyExpandingEnabled || !this.lazyExpandingEnabled) {
oldNode.expandedLazy = false;
}
propertiesChanged = true;
}
return propertiesChanged;
};
scout.Tree.prototype.deleteNode = function(node, parentNode) {
this.deleteNodes([node], parentNode);
};
scout.Tree.prototype.deleteAllNodes = function() {
this.deleteAllChildNodes();
};
scout.Tree.prototype.deleteNodes = function(nodes, parentNode) {
var deletedNodes = [];
nodes = scout.arrays.ensure(nodes).slice(); // copy
nodes.forEach(function(node) {
if (parentNode) {
if (node.parentNode !== parentNode) {
throw new Error('Unexpected parent. Node.parent: ' + node.parentNode + ', parentNode: ' + parentNode);
}
scout.arrays.remove(parentNode.childNodes, node);
} else {
scout.arrays.remove(this.nodes, node);
}
this._destroyTreeNode(node, node.parentNode);
deletedNodes.push(node);
this._updateMarkChildrenChecked(node, false, false);
// remove children from node map
scout.Tree.visitNodes(this._destroyTreeNode.bind(this), node.childNodes);
}, this);
this.deselectNodes(deletedNodes);
// update child node indices
if (parentNode) {
this._updateChildNodeIndex(parentNode.childNodes);
} else {
this._updateChildNodeIndex(this.nodes);
}
// remove node from html document
if (this.rendered) {
this._removeNodes(deletedNodes, parentNode);
}
this.trigger('nodesDeleted', {
nodes: nodes,
parentNode: parentNode
});
};
scout.Tree.prototype.deselectNodes = function(nodes) {
nodes = scout.arrays.ensure(nodes);
var selectedNodes = this.selectedNodes.slice(); // copy
if (scout.arrays.removeAll(selectedNodes, nodes)) {
this.selectNodes(selectedNodes);
}
};
scout.Tree.prototype.deleteAllChildNodes = function(parentNode) {
var nodes;
if (parentNode) {
nodes = parentNode.childNodes;
parentNode.childNodes = [];
} else {
nodes = this.nodes;
this.nodes = [];
}
scout.Tree.visitNodes(updateNodeMap.bind(this), nodes);
// remove node from html document
if (this.rendered) {
this._removeNodes(nodes, parentNode);
}
this.trigger('allChildNodesDeleted', {
parentNode: parentNode
});
// --- Helper functions ---
// Update model and nodemap
function updateNodeMap(node, parentNode) {
this._destroyTreeNode(node, parentNode);
this._updateMarkChildrenChecked(node, false, false);
}
};
scout.Tree.prototype.updateNodeOrder = function(childNodes, parentNode) {
childNodes = scout.arrays.ensure(childNodes);
this._updateChildNodeIndex(childNodes);
if (parentNode) {
if (parentNode.childNodes.length !== childNodes.length) {
throw new Error('Node order may not be updated because lengths of the arrays differ.');
}
// Make a copy so that original array stays untouched
parentNode.childNodes = childNodes.slice();
this._removeChildrenFromFlatList(parentNode, false);
if (parentNode.expanded) {
this._addChildrenToFlatList(parentNode, null, false);
}
} else {
if (this.nodes.length !== childNodes.length) {
throw new Error('Node order may not be updated because lengths of the arrays differ.');
}
// Make a copy so that original array stays untouched
this.nodes = childNodes.slice();
this.nodes.forEach(function(node) {
this._removeFromFlatList(node, false);
this._addToVisibleFlatList(node, false);
if (node.expanded) {
this._addChildrenToFlatList(node, null, false);
}
}, this);
}
this.trigger('childNodeOrderChanged', {
parentNode: parentNode
});
};
scout.Tree.prototype.checkNode = function(node, checked, options) {
var opts = $.extend(options, {
checked: checked
});
this.checkNodes([node], opts);
};
scout.Tree.prototype.checkNodes = function(nodes, options) {
var opts = {
checked: true,
checkOnlyEnabled: true,
checkChildren: this.autoCheckChildren,
triggerNodesChecked: true
};
$.extend(opts, options);
var updatedNodes = [];
// use enabled computed because when the parent of the table is disabled, it should not be allowed to check rows
if (!this.checkable || (!this.enabledComputed && opts.checkOnlyEnabled)) {
return;
}
nodes = scout.arrays.ensure(nodes);
nodes.forEach(function(node) {
if ((!node.enabled && opts.checkOnlyEnabled) || node.checked === opts.checked) {
if (opts.checkChildren) {
this.checkNodes(node.childNodes, opts);
}
return;
}
if (!this.multiCheck && opts.checked) {
for (var i = 0; i < this.checkedNodes.length; i++) {
this.checkedNodes[i].checked = false;
this._updateMarkChildrenChecked(this.checkedNodes[i], false, false, true);
updatedNodes.push(this.checkedNodes[i]);
}
this.checkedNodes = [];
}
node.checked = opts.checked;
if (node.checked) {
this.checkedNodes.push(node);
} else {
scout.arrays.remove(this.checkedNodes, node);
}
updatedNodes.push(node);
this._updateMarkChildrenChecked(node, false, opts.checked, true);
if (opts.checkChildren) {
var childOpts = $.extend({}, opts, {
triggerNodesChecked: false
});
this.checkNodes(node.childNodes, childOpts);
}
}, this);
if (opts.triggerNodesChecked && updatedNodes.length > 0) {
this.trigger('nodesChecked', {
nodes: updatedNodes
});
}
if (this.rendered) {
updatedNodes.forEach(function(node) {
node._renderChecked();
});
}
};
scout.Tree.prototype.uncheckNode = function(node, options) {
var opts = $.extend({
checkOnlyEnabled: true
}, options);
this.uncheckNodes([node], opts);
};
scout.Tree.prototype.uncheckNodes = function(nodes, options) {
var opts = {
checked: false
};
$.extend(opts, options);
this.checkNodes(nodes, opts);
};
scout.Tree.prototype._triggerNodesSelected = function(debounce) {
this.trigger('nodesSelected', {
debounce: debounce
});
};
scout.Tree.prototype._showContextMenu = function(event) {
var func = function(event) {
if (!this.rendered) { // check needed because function is called asynchronously
return;
}
var filteredMenus = this._filterMenus(this.menus, scout.MenuDestinations.CONTEXT_MENU, true),
$part = $(event.currentTarget);
if (filteredMenus.length === 0) {
return; // at least one menu item must be visible
}
// Prevent firing of 'onClose'-handler during contextMenu.open()
// (Can lead to null-access when adding a new handler to this.contextMenu)
if (this.contextMenu) {
this.contextMenu.close();
}
this.contextMenu = scout.create('ContextMenuPopup', {
parent: this,
menuItems: filteredMenus,
location: {
x: event.pageX,
y: event.pageY
},
$anchor: $part,
menuFilter: this._filterMenusHandler
});
this.contextMenu.open();
};
this.session.onRequestsDone(func.bind(this), event);
};
scout.Tree.prototype._onNodeMouseDown = function(event) {
this._doubleClickSupport.mousedown(event);
if (this._doubleClickSupport.doubleClicked()) {
//don't execute on double click events
return false;
}
var $node = $(event.currentTarget);
var node = $node.data('node');
if (!this.hasNode(node)) {
// if node does not belong to this tree, do nothing (may happen if another tree is embedded inside the node)
return;
}
this._$mouseDownNode = $node;
$node.window().one('mouseup', function() {
this._$mouseDownNode = null;
}.bind(this));
this.selectNodes(node);
if (this.checkable && node.enabled && this._isCheckboxClicked(event)) {
if (scout.device.loosesFocusIfPseudoElementIsRemoved()) {
this.focusAndPreventDefault(event);
}
this.checkNode(node, !node.checked);
}
return true;
};
scout.Tree.prototype._onNodeMouseUp = function(event) {
if (this._doubleClickSupport.doubleClicked()) {
//don't execute on double click events
return false;
}
var $node = $(event.currentTarget);
var node = $node.data('node');
if (!this._$mouseDownNode || this._$mouseDownNode[0] !== $node[0]) {
// Don't accept if mouse up happens on another node than mouse down, or mousedown didn't happen on a node at all
return;
}
this.trigger('nodeClick', {
node: node
});
return true;
};
scout.Tree.prototype._isCheckboxClicked = function(event) {
// with CheckableStyle.CHECKBOX_TREE_NODE a click anywhere on the node should trigger the check
if (this.isTreeNodeCheckEnabled()) {
return true;
}
return $(event.target).is('.check-box');
};
scout.Tree.prototype._updateItemPath = function(selectionChanged, ultimate) {
var selectedNodes, node, level;
if (selectionChanged) {
// first remove and select selected
this.groupedNodes = {};
this._inSelectionPathList = {};
}
if (!ultimate) {
// find direct children
selectedNodes = this.selectedNodes;
if (selectedNodes.length === 0) {
return;
}
node = selectedNodes[0];
if (selectionChanged) {
this._inSelectionPathList[node.id] = true;
if (node.childNodes) {
node.childNodes.forEach(function(child) {
this._inSelectionPathList[child.id] = true;
}.bind(this));
}
}
level = node.level;
// find grouping end (ultimate parent)
while (node.parentNode) {
var parent = node.parentNode;
if (this._isGroupingEnd(parent) && !ultimate) {
ultimate = node;
if (!selectionChanged) {
break;
}
}
if (selectionChanged) {
this._inSelectionPathList[parent.id] = true;
}
node = parent;
}
// find group with same ultimate parent
ultimate = ultimate || selectedNodes[0];
this.groupedNodes[ultimate.id] = true;
}
node = ultimate;
if (node && node.expanded && this.groupedNodes[node.id]) {
addToGroup.call(this, node.childNodes);
}
//------ helper function ------//
function addToGroup(nodes) {
nodes.forEach(function(node) {
this.groupedNodes[node.id] = true;
node._decorate();
if (node.expanded && node.isFilterAccepted()) {
addToGroup.call(this, node.childNodes);
}
}.bind(this));
}
};
scout.Tree.prototype._isGroupingEnd = function(node) {
// May be implemented by subclasses, default tree has no grouping parent
return false;
};
/**
* @returns {scout.TreeNode} the first selected node or null when no node is selected.
*/
scout.Tree.prototype.selectedNode = function() {
if (this.selectedNodes.length === 0) {
return null;
}
return this.selectedNodes[0];
};
scout.Tree.prototype.$selectedNodes = function() {
return this.$data.find('.selected');
};
scout.Tree.prototype.$nodes = function() {
return this.$data.find('.tree-node');
};
/**
* @param filter object with createKey() and accept()
*/
scout.Tree.prototype.addFilter = function(filter, doNotFilter, notAnimated) {
if (this._filters.indexOf(filter) < 0) {
this._filters.push(filter);
if (!doNotFilter) {
this.filter(notAnimated);
}
return true;
}
return false;
};
scout.Tree.prototype.removeFilter = function(filter, notAnimated) {
scout.arrays.remove(this._filters, filter);
this.filter(notAnimated);
};
scout.Tree.prototype.filter = function(notAnimated) {
var useAnimation = !!!notAnimated,
changedNodes = [],
newHiddenNodes = [];
// Filter nodes
this.visitNodes(function(node) {
var changed = this._applyFiltersForNode(node);
if (changed) {
changedNodes.push(node);
if (!node.isFilterAccepted()) {
scout.arrays.pushAll(newHiddenNodes, this._removeFromFlatList(node, useAnimation));
} else {
this._addToVisibleFlatList(node, useAnimation);
}
this.viewRangeDirty = true;
} else {
// this else branch is required when the filter-state of a node has not changed
// for instance Node "Telefon mit Sabrina" is visible for filter "tel" and also
// for filter "abr". However, it is possible that the node is _not_ attached, when
// we switch from one filter to another, because the node was not in the view-range
// with the previous filter. That's why we must make sure, the node is attached to
// the DOM, even though the filter state hasn't changed. Otherwise we'd have a
// problem when we insert nodes in this._insertNodeInDOMAtPlace.
this.showNode(node, useAnimation);
}
if ((node.expanded || node.expandedLazy) && node.isFilterAccepted()) {
return false;
}
// don't process children->optimize performance
return true;
}.bind(this));
this._nodesFiltered(newHiddenNodes);
};
/**
* use filtered nodes are removed from visible nodes
*/
scout.Tree.prototype.filterVisibleNodes = function(animated) {
// Filter nodes
var newHiddenNodes = [];
for (var i = 0; i < this.visibleNodesFlat.length; i++) {
var node = this.visibleNodesFlat[i];
var changed = this._applyFiltersForNode(node);
if (changed) {
if (!node.isFilterAccepted()) {
i--;
scout.arrays.pushAll(newHiddenNodes, this._removeFromFlatList(node, animated));
}
this.viewRangeDirty = true;
}
}
this._nodesFiltered(newHiddenNodes);
};
scout.Tree.prototype._nodesFiltered = function(hiddenNodes) {
// non visible nodes must be deselected
this.deselectNodes(hiddenNodes);
};
scout.Tree.prototype._nodeAcceptedByFilters = function(node) {
for (var i = 0; i < this._filters.length; i++) {
var filter = this._filters[i];
if (!filter.accept(node)) {
return false;
}
}
return true;
};
/**
* @returns {Boolean} true if node state has changed, false if not
*/
scout.Tree.prototype._applyFiltersForNode = function(node) {
var changed = node.filterDirty;
if (this._nodeAcceptedByFilters(node)) {
if (!node.filterAccepted) {
node.filterAccepted = true;
changed = true;
}
} else {
if (node.filterAccepted) {
node.filterAccepted = false;
changed = true;
}
}
if (changed) {
node.filterDirty = false;
node.childNodes.forEach(function(childNode) {
childNode.filterDirty = true;
});
return true;
}
return false;
};
/**
* Just insert node in DOM. NO check if in viewRange
*/
scout.Tree.prototype._insertNodesInDOM = function(nodes, indexHint) {
if (!this.rendered && !this.rendering) {
return;
}
nodes.forEach(function(node) {
var index = indexHint === undefined ? this.visibleNodesFlat.indexOf(node) : indexHint;
if (index === -1 || !(this.viewRangeRendered.from + this.viewRangeSize >= index && this.viewRangeRendered.from <= index && this.viewRangeRendered.size() > 0) || node.attached) {
//node is not visible
return;
}
if (!node.rendered) {
this._renderNode(node);
}
node._decorate();
this._insertNodeInDOMAtPlace(node, index);
if (this.prevSelectedNode === node) {
this._highlightPrevSelectedNode();
}
node.rendered = true;
node.attached = true;
}, this);
this._installNodes(nodes);
};
scout.Tree.prototype._installNodes = function(nodes) {
// The measuring is separated into 3 blocks for performance reasons -> separates reading and setting of styles
// 1. Prepare style for measuring
if (this.isHorizontalScrollingEnabled()) {
nodes.forEach(function(node) {
node.$node.css('width', 'auto');
node.$node.css('display', 'inline-block');
}, this);
}
// 2. Measure
nodes.forEach(function(node) {
node.height = node.$node.outerHeight(true);
if (!this.isHorizontalScrollingEnabled()) {
return;
}
var newWidth = node.$node.css('width');
var widthBackup = node.width ? node.width : 0;
if (widthBackup === this.maxNodeWidth && newWidth < this.maxNodeWidth) {
this.maxNodeWidth = 0;
this.nodeWidthDirty = true;
} else if (newWidth > this.maxNodeWidth) {
this.maxNodeWidth = newWidth;
this.nodeWidthDirty = true;
}
node.width = newWidth;
}, this);
// 3. Reset style
if (this.isHorizontalScrollingEnabled()) {
nodes.forEach(function(node) {
if (!this.nodeWidthDirty) {
node.$node.css('width', this.maxNodeWidth);
}
node.$node.css('display', '');
}, this);
}
this._updateDomNodeWidth(nodes);
};
/**
* Attaches node to DOM, if it is visible and in view range
* */
scout.Tree.prototype._ensureNodeInDOM = function(node, useAnimation, indexHint) {
if (node && !node.attached && node === this.visibleNodesFlat[indexHint] && indexHint >= this.viewRangeRendered.from && indexHint < this.viewRangeRendered.to) {
this.showNode(node, useAnimation, indexHint);
}
};
scout.Tree.prototype._insertNodeInDOMAtPlace = function(node, index) {
var $node = node.$node;
if (index === 0) {
if (this.$fillBefore) {
$node.insertAfter(this.$fillBefore);
} else {
this.$data.prepend($node);
}
return;
}
// append after index
var nodeBefore = this.visibleNodesFlat[index - 1];
this._ensureNodeInDOM(nodeBefore, false, index - 1);
if (nodeBefore.attached) {
$node.insertAfter(nodeBefore.$node);
return;
}
if (index + 1 < this.visibleNodesFlat.length) {
var nodeAfter = this.visibleNodesFlat[index + 1];
if (nodeAfter.attached) {
$node.insertBefore(nodeAfter.$node);
return;
}
}
// used when the tree is scrolled
if (this.$fillBefore) {
$node.insertAfter(this.$fillBefore);
} else {
this.$data.prepend($node);
}
};
scout.Tree.prototype.showNode = function(node, useAnimation, indexHint) {
if (node.attached || !this.rendered) {
return;
}
this._ensureNodeInDOM(node.parentNode, useAnimation, indexHint - 1);
this._insertNodesInDOM([node], indexHint);
if (!node.rendered) {
return;
}
var $node = node.$node;
if ($node.is('.showing')) {
return;
}
$node.addClass('showing');
$node.removeClass('hiding');
var that = this;
if (useAnimation) {
var oldStyle = $node.attr('style');
var oldVisible = $node.isVisible();
$node.setVisible(false);
$node.stop().slideDown({
duration: 250,
start: that.startAnimationFunc,
complete: function() {
that.runningAnimationsFinishFunc();
$node.removeClass('showing');
$node.setVisible(oldVisible);
$node.attrOrRemove('style', oldStyle);
}
});
}
};
scout.Tree.prototype.hideNode = function(node, useAnimation, suppressDetachHandling) {
if (!node.attached) {
return;
}
this.viewRangeDirty = true;
var that = this,
$node = node.$node;
if (!$node) {
//node is not rendered
return;
}
if ($node.is('.hiding')) {
return;
}
$node.addClass('hiding');
$node.removeClass('showing');
if (useAnimation) {
this._renderViewportBlocked = true;
var oldStyle = $node.attr('style');
$node.stop().slideUp({
duration: 250,
start: that.startAnimationFunc,
complete: function() {
that.runningAnimationsFinishFunc();
$node.detach();
node.attached = false;
$node.attrOrRemove('style', oldStyle);
}
});
} else if (!suppressDetachHandling) {
$node.detach();
node.attached = false;
that.invalidateLayoutTree();
}
};
scout.Tree.prototype._nodesToIds = function(nodes) {
return nodes.map(function(node) {
return node.id;
});
};
scout.Tree.prototype._nodesByIds = function(ids) {
return ids.map(function(id) {
return this.nodesMap[id];
}.bind(this));
};
scout.Tree.prototype._nodeById = function(id) {
return this.nodesMap[id];
};
scout.Tree.prototype.hasNode = function(node) {
return !!this._nodeById(node.id);
};
scout.Tree.prototype._onNodeDoubleClick = function(event) {
if (this.isBreadcrumbStyleActive()) {
return;
}
var $node = $(event.currentTarget);
var node = $node.data('node');
var expanded = !$node.hasClass('expanded');
this.doNodeAction(node, expanded);
};
scout.Tree.prototype.doNodeAction = function(node, expanded) {
this.trigger('nodeAction', {
node: node
});
// For CheckableStyle.CHECKBOX_TREE_NODE expand on double click is only enabled for disabled nodes. Otherwise it would conflict with the "check on node click" behavior.
if (!(this.checkable === true && this.isTreeNodeCheckEnabled() && node.enabled)) {
this.setNodeExpanded(node, expanded, {
lazy: false // always show all nodes on node double click
});
}
};
scout.Tree.prototype._onNodeControlMouseDown = function(event) {
this._doubleClickSupport.mousedown(event);
if (this._doubleClickSupport.doubleClicked()) {
//don't execute on double click events
return false;
}
var $node = $(event.currentTarget).parent();
var node = $node.data('node');
var expanded = !$node.hasClass('expanded');
var expansionOpts = {
lazy: false // always show all nodes when the control gets clicked
};
// Click on "show all" control shows all nodes
if ($node.hasClass('lazy')) {
if (event.ctrlKey || event.shiftKey) {
// Collapse
expanded = false;
expansionOpts.collapseChildNodes = true;
} else {
// Show all nodes
this.expandNode(node, expansionOpts);
return false;
}
}
// Because we suppress handling by browser we have to set focus manually
if (this.requestFocusOnNodeControlMouseDown) {
this.focus();
}
this.selectNodes(node); // <---- ### 1
this.setNodeExpanded(node, expanded, expansionOpts); // <---- ### 2
// prevent bubbling to _onNodeMouseDown()
$.suppressEvent(event);
// ...but return true, so Outline.js can override this method and check if selection has been changed or not
return true;
};
scout.Tree.prototype._onNodeControlMouseUp = function(event) {
// prevent bubbling to _onNodeMouseUp()
return false;
};
scout.Tree.prototype._onNodeControlDoubleClick = function(event) {
// prevent bubbling to _onNodeDoubleClick()
return false;
};
scout.Tree.prototype._onContextMenu = function(event) {
event.preventDefault();
this._showContextMenu(event);
};
scout.Tree.prototype.changeNode = function(node) {
if (this._applyFiltersForNode(node)) {
if (node.isFilterAccepted()) {
this._addToVisibleFlatList(node, false);
} else {
this._removeFromFlatList(node, false);
}
}
if (this.rendered) {
node._decorate();
}
this.trigger('nodeChanged', {
node: node
});
};
// same as on scout.Table.prototype._onDesktopPopupOpen
scout.Tree.prototype._onDesktopPopupOpen = function(event) {
var popup = event.popup;
if (!this.enabled) {
return;
}
// Set tree style to focused if a context menu or a menu bar popup opens, so that it looks as it still has the focus
if (this.has(popup) && popup instanceof scout.ContextMenuPopup) {
this.$container.addClass('focused');
popup.one('destroy', function() {
if (this.rendered) {
this.$container.removeClass('focused');
}
}.bind(this));
}
};
scout.Tree.prototype.updateScrollbars = function() {
scout.scrollbars.update(this.$data);
};
/* --- STATIC HELPERS ------------------------------------------------------------- */
/**
* @memberOf scout.Tree
*/
scout.Tree.collectSubtree = function($rootNode, includeRootNodeInResult) {
if (!$rootNode) {
return $();
}
var rootLevel = parseFloat($rootNode.attr('data-level'));
// Find first node after the root element that has the same or a lower level
var $nextNode = $rootNode.next();
while ($nextNode.length > 0) {
var level = parseFloat($nextNode.attr('data-level'));
if (isNaN(level) || level <= rootLevel) {
break;
}
$nextNode = $nextNode.next();
}
// The result set consists of all nodes between the root node and the found node
var $result = $rootNode.nextUntil($nextNode);
if (includeRootNodeInResult === undefined || includeRootNodeInResult) {
$result = $result.add($rootNode);
}
return $result;
};
/**
* pre-order (top-down) traversal of the tree-nodes provided.<br>
* if func returns true the children of the visited node are not visited.
*/
scout.Tree.visitNodes = function(func, nodes, parentNode) {
var i, node;
if (!nodes) {
return;
}
for (i = 0; i < nodes.length; i++) {
node = nodes[i];
var doNotProcessChildren = func(node, parentNode);
if (!doNotProcessChildren && node.childNodes.length > 0) {
scout.Tree.visitNodes(func, node.childNodes, node);
}
}
};