@eclipse-scout/core
Version:
Eclipse Scout runtime
487 lines (416 loc) • 13 kB
text/typescript
/*
* Copyright (c) 2010, 2025 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
import {aria, FilterElement, icons, InitModelOf, objectFactoryHints, objects, ObjectWithType, scout, Session, SomeRequired, styles, texts, Tree, TreeNodeModel} from '../index';
import $ from 'jquery';
export class TreeNode implements TreeNodeModel, ObjectWithType, FilterElement {
declare model: TreeNodeModel;
declare initModel: SomeRequired<this['model'], 'parent'>;
objectType: string;
checked: boolean;
childNodes: TreeNode[];
cssClass: string;
enabled: boolean;
expanded: boolean;
expandedLazy: boolean;
htmlEnabled: boolean;
iconId: string;
id: string;
initialExpanded: boolean;
lazyExpandingEnabled: boolean;
leaf: boolean;
level: number;
parent: Tree;
parentNode: TreeNode;
session: Session;
text: string;
tooltipText: string;
foregroundColor: string;
backgroundColor: string;
font: string;
initialized: boolean;
rendered: boolean;
attached: boolean;
destroyed: boolean;
filterAccepted: boolean;
filterDirty: boolean;
childrenLoaded: boolean;
childrenChecked: boolean;
height: number;
width: number;
displayBackup: string;
prevSelectionAnimationDone: boolean;
$node: JQuery;
$text: JQuery<HTMLSpanElement>;
childNodeIndex: number;
/**
* This internal variable stores the promise which is used when a loadChildren() operation is in progress.
*/
protected _loadChildrenPromise: JQuery.Promise<any>;
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._loadChildrenPromise = null;
}
init(model: InitModelOf<this>) {
let 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.
*/
protected _destroy() {
// NOP
}
/**
* @deprecated use {@link tree} instead.
*/
getTree(): Tree {
return this.tree;
}
get tree(): Tree {
return this.parent;
}
protected _init(model: InitModelOf<this>) {
scout.assertParameter('parent', model.parent, Tree);
this.session = model.session || model.parent.session;
$.extend(this, model);
this._resolveTextKeys(['text']);
this._resolveIconIds(['iconId']);
// make sure all child nodes are TreeNodes too
if (this.hasChildNodes()) {
this.tree.ensureTreeNodes(this.childNodes, this);
}
}
protected _resolveTextKeys(properties: string[]) {
texts.resolveTextProperties(this, properties);
}
protected _resolveIconIds(properties: string[]) {
icons.resolveIconProperties(this, properties);
}
protected _jsonModel(): TreeNodeModel {
return null;
}
reset() {
if (this.$node) {
this.$node.remove();
this.$node = null;
}
this.rendered = false;
this.attached = false;
}
hasChildNodes(): boolean {
return this.childNodes.length > 0;
}
/**
* @returns true, if this node is an ancestor of the given node
*/
isAncestorOf(node: TreeNode): boolean {
while (node) {
if (node.parentNode === this) {
return true;
}
node = node.parentNode;
}
return false;
}
/**
* @returns true, if the node is a descendant of the given node
*/
isDescendantOf(node: TreeNode): boolean {
if (node === this.parentNode) {
return true;
}
if (!this.parentNode) {
return false;
}
return this.parentNode.isDescendantOf(node);
}
setFilterAccepted(filterAccepted: boolean) {
this.filterAccepted = filterAccepted;
}
/**
* This method loads the child nodes of this node and returns a jQuery.Promise to register callbacks
* when loading is done or has failed. To skip loading the children when they are already loaded, use
* {@link #ensureLoadChildren} instead.
*
* @returns a Promise or null when TreeNode cannot load children (which is the case for all
* TreeNodes in the remote case). The default impl. returns an empty resolved promise.
*/
loadChildren(): JQuery.Promise<any> {
return $.resolvedPromise();
}
/**
* This method calls loadChildren() but does nothing when children are already loaded or when loadChildren()
* is already in progress.
*/
ensureLoadChildren(): JQuery.Promise<any> {
// 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;
}
let promise = this.loadChildren();
if (promise.state() === 'resolved') {
this._loadChildrenPromise = null;
return promise;
}
this._loadChildrenPromise = promise;
promise.then(this._onLoadChildrenDone.bind(this));
return promise; // we must always return a promise, never null - otherwise caller would throw an error
}
protected _onLoadChildrenDone() {
this._loadChildrenPromise = null;
}
/**
* This functions renders sets the $node and $text properties.
*
* @param $parent the tree DOM
* @param paddingLeft calculated by tree
*/
render($parent: JQuery, paddingLeft: number) {
this.$node = $parent.makeDiv('tree-node')
.data('node', this)
.attr('data-nodeid', this.id)
.attr('data-level', this.level);
aria.role(this.$node, 'treeitem');
aria.level(this.$node, this.level + 1); // starts counting from 1
if (!objects.isNullOrUndefined(paddingLeft)) {
this.$node.cssPaddingLeft(paddingLeft);
}
this.$text = this.$node.appendSpan('text');
this._renderControl();
if (this.tree.checkable) {
this._renderCheckbox();
}
this._renderText();
this._renderIcon();
}
setText(text: string) {
this.text = text;
}
protected _renderText() {
if (this.htmlEnabled) {
this.$text.html(this.text);
} else {
this.$text.textOrNbsp(this.text);
}
}
setChecked(checked: boolean) {
this.checked = checked;
}
/** @internal */
_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);
aria.checked(this.$node, this.checked);
}
setIconId(iconId: string) {
this.iconId = iconId;
}
protected _renderIcon() {
this.$node.toggleClass('has-icon', !!this.iconId);
this.$node.icon(this.iconId, $icon => $icon.insertBefore(this.$text));
}
$icon(): JQuery {
return this.$node.children('.icon');
}
protected _renderControl() {
let $control = this.$node.prependDiv('tree-node-control');
this._updateControl($control);
}
/** @internal */
_updateControl($control: JQuery) {
let tree = this.tree;
$control.toggleClass('checkable', tree.checkable);
$control.cssPaddingLeft(tree._computeNodeControlPaddingLeft(this));
$control.setVisible(!this.leaf);
}
/** @internal */
_renderCheckbox() {
let $checkboxContainer = this.$node.prependDiv('tree-node-checkbox');
let $checkbox = $checkboxContainer
.appendDiv('check-box')
.toggleClass('checked', this.checked)
.toggleClass('disabled', !this.enabled);
aria.role($checkbox, 'checkbox');
aria.checked($checkbox, this.checked);
aria.checked(this.$node, this.checked);
$checkbox.toggleClass('children-checked', !!this.childrenChecked);
this._renderChildrenChecked();
}
/** @internal */
_renderChildrenChecked() {
// if node is not rendered, do nothing
if (!this.$node) {
return;
}
this.$node.children('.tree-node-checkbox')
.children('.check-box')
.toggleClass('children-checked', !!this.childrenChecked);
}
/** @internal */
_decorate() {
// This node is not yet rendered, nothing to do
if (!this.$node) {
return;
}
let $node = this.$node;
let tree = this.tree;
$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', !this.enabled);
aria.disabled($node, $node.hasClass('disabled') || null);
aria.expanded($node, $node.hasClass('leaf') ? null : $node.hasClass('expanded'));
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');
}
if (this.parentNode) {
aria.posinset($node, this.childNodeIndex + 1); // starts counting from 1
aria.setsize($node, this.parentNode.childNodes.length);
}
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) {
let hasVisibleNodes = this.parentNode.childNodes.some(childNode => !!tree.visibleNodesMap[childNode.id]);
if (!hasVisibleNodes && this.parentNode.$node) {
// Remove 'lazy' from parent
this.parentNode.$node.removeClass('lazy');
}
}
}
/**
* @returns The object that has the properties used for styles (colors, fonts, etc.)
* The default impl. returns "this". Override this function to return another object.
*/
protected _getStyles(): object {
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 be determined by the node itself.
*/
protected _preserveCssClasses($node: JQuery): string {
let 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;
}
setCssClass(cssClass: string) {
this.cssClass = cssClass;
}
setEnabled(enabled: boolean) {
this.enabled = enabled;
}
setExpanded(expanded: boolean) {
this.expanded = expanded;
}
setExpandedLazy(expandedLazy: boolean) {
this.expandedLazy = expandedLazy;
}
setLazyExpandingEnabled(lazyExpandingEnabled: boolean) {
this.lazyExpandingEnabled = lazyExpandingEnabled;
}
setInitialExpanded(initialExpanded: boolean) {
this.initialExpanded = initialExpanded;
}
setLeaf(leaf: boolean) {
this.leaf = leaf;
}
setLevel(level: number) {
this.level = level;
}
setHtmlEnabled(htmlEnabled: boolean) {
this.htmlEnabled = htmlEnabled;
}
setParentNode(parentNode: TreeNode) {
this.parentNode = parentNode;
}
setTooltipText(tooltipText: string) {
this.tooltipText = tooltipText;
}
setBackgroundColor(color: string) {
this.backgroundColor = color;
}
setForegroundColor(color: string) {
this.foregroundColor = color;
}
setFont(font: string) {
this.font = font;
}
}