blob: 1e4bd97f289172e9a57076c68c2f0849b8fe8001 [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
*/
import {defaultValues, icons, objects, scout, styles, texts, Tree} from '../index';
import * as $ from 'jquery';
/**
* @class
*/
export default class TreeNode {
constructor() {
this.$node = null;
this.$text = null;
this.attached = false;
this.checked = false;
this.childNodes = [];
this.childrenLoaded = false;
this.childrenChecked = false;
this.cssClass = null;
this.destroyed = false;
this.enabled = true;
this.expanded = false;
this.expandedLazy = false;
this.filterAccepted = true;
this.filterDirty = false;
this.htmlEnabled = false;
this.iconId = null;
this.id = null;
this.initialized = false;
this.initialExpanded = false;
this.lazyExpandingEnabled = false;
this.leaf = false;
this.level = 0;
this.parent = null;
this.parentNode = undefined;
this.prevSelectionAnimationDone = false;
this.rendered = false;
this.session = null;
this.text = null;
/**
* This internal variable stores the promise which is used when a loadChildren() operation is in progress.
*/
this._loadChildrenPromise = false;
}
init(model) {
var staticModel = this._jsonModel();
if (staticModel) {
model = $.extend({}, staticModel, model);
}
this._init(model);
if (model.initialExpanded === undefined) {
this.initialExpanded = this.expanded;
}
}
destroy() {
if (this.destroyed) {
// Already destroyed, do nothing
return;
}
this._destroy();
this.destroyed = true;
}
/**
* Override this method to do something when TreeNode gets destroyed. The default impl. does nothing.
*/
_destroy() {
// NOP
}
getTree() {
return this.parent;
}
_init(model) {
scout.assertParameter('parent', model.parent, Tree);
this.session = model.session || model.parent.session;
$.extend(this, model);
defaultValues.applyTo(this);
texts.resolveTextProperty(this, 'text');
icons.resolveIconProperty(this, 'iconId');
// make sure all child nodes are TreeNodes too
if (this.hasChildNodes()) {
this.getTree()._ensureTreeNodes(this.childNodes);
}
}
_jsonModel() {
}
reset() {
if (this.$node) {
this.$node.remove();
this.$node = null;
}
this.rendered = false;
this.attached = false;
}
hasChildNodes() {
return this.childNodes.length > 0;
}
/**
* @returns {boolean} true, if the node is an ancestor of the given node
*/
isAncestorOf(node) {
while (node) {
if (node.parentNode === this) {
return true;
}
node = node.parentNode;
}
return false;
}
/**
* @returns {boolean} true, if the node is a descendant of the given node
*/
isDescendantOf(node) {
if (node === this.parentNode) {
return true;
}
if (!this.parentNode) {
return false;
}
// noinspection JSDeprecatedSymbols
return this.parentNode.isDescendantOf(node);
}
/**
* @deprecated use isDescendantOf instead
*/
isChildOf(node) {
return this.isDescendantOf(node);
}
isFilterAccepted(forceFilter) {
if (this.filterDirty || forceFilter) {
this.getTree()._applyFiltersForNode(this);
}
return this.filterAccepted;
}
/**
* This method loads the child nodes of this node and returns a jQuery.Deferred to register callbacks
* when loading is done or has failed. This method should only be called when childrenLoaded is false.
*
* @return {$.Deferred} or null when TreeNode cannot load children (which is the case for all
* TreeNodes in the remote case). The default impl. return null.
*/
loadChildren() {
return $.resolvedDeferred();
}
/**
* This method calls loadChildren() but does nothing when children are already loaded or when loadChildren()
* is already in progress.
* @returns {Promise}
*/
ensureLoadChildren() {
// when children are already loaded we return an already resolved promise so the caller can continue immediately
if (this.childrenLoaded) {
return $.resolvedPromise();
}
// when load children is already in progress, we return the same promise
if (this._loadChildrenPromise) {
return this._loadChildrenPromise;
}
var deferred = this.loadChildren();
var promise = deferred.promise();
// check if we can get rid of this state-check in a future release
if (deferred.state() === 'resolved') {
this._loadChildrenPromise = null;
return promise;
}
this._loadChildrenPromise = promise;
promise.done(this._onLoadChildrenDone.bind(this));
return promise; // we must always return a promise, never null - otherwise caller would throw an error
}
_onLoadChildrenDone() {
this._loadChildrenPromise = null;
}
setText(text) {
this.text = text;
}
/**
* This functions renders sets the $node and $text properties.
*
* @param {jQuery} $parent the tree DOM
* @param {number} paddingLeft calculated by tree
*/
render($parent, paddingLeft) {
this.$node = $parent.makeDiv('tree-node')
.data('node', this)
.attr('data-nodeid', this.id)
.attr('data-level', this.level);
if (!objects.isNullOrUndefined(paddingLeft)) {
this.$node.css('padding-left', paddingLeft);
}
this.$text = this.$node.appendSpan('text');
this._renderControl();
if (this.getTree().checkable) {
this._renderCheckbox();
}
this._renderText();
this._renderIcon();
}
_renderText() {
if (this.htmlEnabled) {
this.$text.html(this.text);
} else {
this.$text.textOrNbsp(this.text);
}
}
_renderChecked() {
// if node is not rendered, do nothing
if (!this.rendered) {
return;
}
this.$node
.children('.tree-node-checkbox')
.children('.check-box')
.toggleClass('checked', this.checked);
}
_renderIcon() {
this.$node.icon(this.iconId, function($icon) {
$icon.insertBefore(this.$text);
}.bind(this));
}
$icon() {
return this.$node.children('.icon');
}
_renderControl() {
var $control = this.$node.prependDiv('tree-node-control');
this._updateControl($control, this.getTree());
}
_updateControl($control, tree) {
$control.toggleClass('checkable', tree.checkable);
$control.cssPaddingLeft(tree.nodeControlPaddingLeft + this.level * tree.nodePaddingLevel);
$control.setVisible(!this.leaf);
}
_renderCheckbox() {
var $checkboxContainer = this.$node.prependDiv('tree-node-checkbox');
var $checkbox = $checkboxContainer
.appendDiv('check-box')
.toggleClass('checked', this.checked)
.toggleClass('disabled', !(this.getTree().enabled && this.enabled));
$checkbox.toggleClass('children-checked', !!this.childrenChecked);
}
_decorate() {
// This node is not yet rendered, nothing to do
if (!this.$node) {
return;
}
var $node = this.$node,
tree = this.getTree();
$node.attr('class', this._preserveCssClasses($node));
$node.addClass(this.cssClass);
$node.toggleClass('leaf', !!this.leaf);
$node.toggleClass('expanded', (!!this.expanded && this.childNodes.length > 0));
$node.toggleClass('lazy', $node.hasClass('expanded') && this.expandedLazy);
$node.toggleClass('group', !!tree.groupedNodes[this.id]);
$node.setEnabled(!!this.enabled);
$node.children('.tree-node-control').setVisible(!this.leaf);
$node
.children('.tree-node-checkbox')
.children('.check-box')
.toggleClass('disabled', !(tree.enabled && this.enabled));
if (!this.parentNode && tree.selectedNodes.length === 0 || // root nodes have class child-of-selected if no node is selected
tree._isChildOfSelectedNodes(this)) {
$node.addClass('child-of-selected');
}
this._renderText();
this._renderIcon();
styles.legacyStyle(this._getStyles(), $node);
// If parent node is marked as 'lazy', check if any visible child nodes remain.
if (this.parentNode && this.parentNode.expandedLazy) {
var hasVisibleNodes = this.parentNode.childNodes.some(function(childNode) {
return !!tree.visibleNodesMap[childNode.id];
});
if (!hasVisibleNodes && this.parentNode.$node) {
// Remove 'lazy' from parent
this.parentNode.$node.removeClass('lazy');
}
}
}
/**
* @return {object} The object that has the properties used for styles (colors, fonts, etc.)
* The default impl. returns "this". Override this function to return another object.
*/
_getStyles() {
return this;
}
/**
* This function extracts all CSS classes that are set externally by the tree.
* The classes depend on the tree hierarchy or the selection and thus cannot determined
* by the node itself.
*/
_preserveCssClasses($node) {
var cssClass = 'tree-node';
if ($node.isSelected()) {
cssClass += ' selected';
}
if ($node.hasClass('ancestor-of-selected')) {
cssClass += ' ancestor-of-selected';
}
if ($node.hasClass('parent-of-selected')) {
cssClass += ' parent-of-selected';
}
return cssClass;
}
_updateIconWidth() {
var cssWidth = '';
if (this.iconId) {
// always add 1 pixel to the result of outer-width to prevent rendering errors in IE, where
// the complete text is replaced by an ellipsis, when the .text element is a bit too large
cssWidth = 'calc(100% - ' + (this.$icon().outerWidth() + 1) + 'px)';
}
this.$text.css('max-width', cssWidth);
}
}