org.dominokit.domino.ui.tree.TreeItem Maven / Gradle / Ivy
/*
* Copyright © 2019 Dominokit
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dominokit.domino.ui.tree;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.dominokit.domino.ui.style.Unit.px;
import static org.jboss.elemento.Elements.a;
import static org.jboss.elemento.Elements.div;
import static org.jboss.elemento.Elements.li;
import static org.jboss.elemento.Elements.span;
import static org.jboss.elemento.Elements.ul;
import elemental2.dom.DomGlobal;
import elemental2.dom.EventListener;
import elemental2.dom.HTMLAnchorElement;
import elemental2.dom.HTMLElement;
import elemental2.dom.HTMLLIElement;
import elemental2.dom.HTMLUListElement;
import elemental2.dom.Node;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import org.dominokit.domino.ui.collapsible.Collapsible;
import org.dominokit.domino.ui.icons.BaseIcon;
import org.dominokit.domino.ui.icons.Icons;
import org.dominokit.domino.ui.style.Style;
import org.dominokit.domino.ui.style.Styles;
import org.dominokit.domino.ui.style.WaveColor;
import org.dominokit.domino.ui.style.WaveStyle;
import org.dominokit.domino.ui.style.WavesElement;
import org.dominokit.domino.ui.utils.CanActivate;
import org.dominokit.domino.ui.utils.CanDeactivate;
import org.dominokit.domino.ui.utils.DominoElement;
import org.dominokit.domino.ui.utils.DominoUIConfig;
import org.dominokit.domino.ui.utils.HasClickableElement;
import org.dominokit.domino.ui.utils.ParentTreeItem;
import org.jboss.elemento.EventType;
import org.jboss.elemento.IsElement;
/**
* A component representing the tree item
*
* @param the type of the value object inside the item
* @see WavesElement
* @see ParentTreeItem
* @see CanActivate
* @see CanDeactivate
* @see HasClickableElement
*/
public class TreeItem extends WavesElement>
implements ParentTreeItem>, CanActivate, CanDeactivate, HasClickableElement {
private String title;
private HTMLLIElement element;
private final DominoElement anchorElement;
private final List> subItems = new LinkedList<>();
private TreeItem activeTreeItem;
private ParentTreeItem> parent;
private Collapsible collapsible;
private HTMLUListElement childrenContainer;
private BaseIcon> icon;
private BaseIcon> activeIcon;
private BaseIcon> originalIcon;
private BaseIcon> expandIcon;
private T value;
private int level = 1;
private int levelPadding = 15;
private ToggleTarget toggleTarget = ToggleTarget.ANY;
private final DominoElement indicatorContainer =
DominoElement.of(span()).css("tree-indicator");
private HTMLElement titleElement;
private OriginalState originalState;
public TreeItem(String title, BaseIcon> icon) {
this.title = title;
setIcon(icon);
titleElement = DominoElement.of(span()).css("title").textContent(title).element();
DominoElement toggleContainer = DominoElement.of(span()).css("tree-tgl-icn");
this.anchorElement =
DominoElement.of(a())
.add(this.icon)
.add(
DominoElement.of(div())
.css(Styles.ellipsis_text)
.style("margin-top: 2px;")
.add(titleElement))
.add(
toggleContainer
.appendChild(
Icons.ALL
.plus_mdi()
.size18()
.css("tree-tgl-collapsed")
.clickable()
.addClickListener(
evt -> {
evt.stopPropagation();
toggle();
activateItem();
}))
.appendChild(
Icons.ALL
.minus_mdi()
.size18()
.css("tree-tgl-expanded")
.clickable()
.addClickListener(
evt -> {
evt.stopPropagation();
toggle();
activateItem();
})))
.add(indicatorContainer);
init();
}
public TreeItem(String title) {
this(title, Icons.ALL.folder().setCssProperty("visibility", "hidden"));
}
public TreeItem(BaseIcon> icon) {
setIcon(icon);
this.anchorElement = DominoElement.of(a().add(this.icon));
init();
}
public TreeItem(String title, T value) {
this(title);
this.value = value;
}
public TreeItem(String title, BaseIcon> icon, T value) {
this(title, icon);
this.value = value;
}
public TreeItem(BaseIcon> icon, T value) {
this(icon);
this.value = value;
}
/**
* Creates new tree item with a title
*
* @param title the title of the item
* @return new instance
*/
public static TreeItem create(String title) {
TreeItem treeItem = new TreeItem<>(title);
treeItem.value = title;
return treeItem;
}
/**
* Creates new tree item with a title and an icon
*
* @param title the title of the item
* @param icon the item's {@link BaseIcon}
* @return new instance
*/
public static TreeItem create(String title, BaseIcon> icon) {
TreeItem treeItem = new TreeItem<>(title, icon);
treeItem.value = title;
return treeItem;
}
/**
* Creates new tree item with an icon
*
* @param icon the item's {@link BaseIcon}
* @return new instance
*/
public static TreeItem create(BaseIcon> icon) {
TreeItem treeItem = new TreeItem<>(icon);
treeItem.value = "";
return treeItem;
}
/**
* Creates new tree item with a title and a value
*
* @param title the title of the item
* @param value the value of the item
* @param the type of the value
* @return new instance
*/
public static TreeItem create(String title, T value) {
return new TreeItem<>(title, value);
}
/**
* Creates new tree item with a title, an icon and a value
*
* @param title the title of the item
* @param icon the item's {@link BaseIcon}
* @param value the value of the item
* @param the type of the value
* @return new instance
*/
public static TreeItem create(String title, BaseIcon> icon, T value) {
return new TreeItem<>(title, icon, value);
}
/**
* Creates new tree item with an icon and a value
*
* @param icon the item's {@link BaseIcon}
* @param value the value of the item
* @param the type of the value
* @return new instance
*/
public static TreeItem create(BaseIcon> icon, T value) {
return new TreeItem<>(icon, value);
}
private void init() {
this.element = li().element();
this.element.appendChild(anchorElement.element());
childrenContainer = DominoElement.of(ul()).css("ml-tree").element();
element().appendChild(childrenContainer);
collapsible =
Collapsible.create(childrenContainer)
.setStrategy(DominoUIConfig.INSTANCE.getDefaultTreeCollapseStrategySupplier().get(this))
.addHideHandler(
() -> {
anchorElement.removeCss("toggled");
restoreIcon();
})
.addShowHandler(
() -> {
anchorElement.addCss("toggled");
replaceIcon(expandIcon);
})
.hide();
anchorElement.addEventListener(
"click",
evt -> {
if (ToggleTarget.ANY.equals(this.toggleTarget) && isParent()) {
toggle();
}
activateItem();
});
init(this);
setToggleTarget(ToggleTarget.ANY);
setWaveColor(WaveColor.THEME);
applyWaveStyle(WaveStyle.BLOCK);
}
private void activateItem() {
if (nonNull(TreeItem.this.getActiveItem())) {
TreeItem.this.activeTreeItem.deactivate();
TreeItem.this.activeTreeItem = null;
}
parent.setActiveItem(TreeItem.this);
}
/**
* Adds a child item to this one
*
* @param treeItem the child {@link TreeItem}
* @return same instance
*/
public TreeItem appendChild(TreeItem treeItem) {
this.subItems.add(treeItem);
childrenContainer.appendChild(treeItem.element());
anchorElement.addCss("tree-toggle");
treeItem.parent = this;
treeItem.setLevel(level + 1);
treeItem.addCss("tree-leaf");
Style.of(this.element()).removeCss("tree-leaf");
treeItem.setToggleTarget(this.toggleTarget);
treeItem.setLevelPadding(levelPadding);
this.style().addCss("tree-item-parent");
return this;
}
/**
* Adds new separator
*
* @return same instance
*/
public TreeItem addSeparator() {
childrenContainer.appendChild(DominoElement.of(li()).css("separator").add(a()).element());
return this;
}
/**
* Sets what is the target for toggling an item
*
* @param toggleTarget the {@link ToggleTarget}
* @return same instance
*/
public TreeItem setToggleTarget(ToggleTarget toggleTarget) {
if (nonNull(toggleTarget)) {
if (nonNull(this.toggleTarget)) {
this.removeCss(this.toggleTarget.getStyle());
}
this.toggleTarget = toggleTarget;
this.css(this.toggleTarget.getStyle());
if (ToggleTarget.ICON.equals(toggleTarget)) {
if (nonNull(icon)) {
icon.setClickable(true);
}
} else {
if (nonNull(icon)) {
icon.setClickable(false);
}
}
subItems.forEach(item -> item.setToggleTarget(toggleTarget));
}
return this;
}
private void toggle() {
if (isParent()) {
collapsible.toggleDisplay();
}
}
/** {@inheritDoc} */
@Override
public TreeItem show() {
return show(false);
}
/**
* Shows the item
*
* @param expandParent true to expand the parent of the item
* @return same instance
*/
public TreeItem show(boolean expandParent) {
if (isParent()) {
collapsible.show();
}
if (expandParent && nonNull(parent)) {
parent.expand(true);
}
return this;
}
/** {@inheritDoc} */
@Override
public TreeItem expand() {
return show();
}
/** {@inheritDoc} */
@Override
public TreeItem expand(boolean expandParent) {
return show(expandParent);
}
/** {@inheritDoc} */
@Override
public TreeItem hide() {
if (isParent()) {
collapsible.hide();
}
return this;
}
/** {@inheritDoc} */
@Override
public TreeItem toggleDisplay() {
if (isParent()) {
collapsible.toggleDisplay();
}
return this;
}
/** @deprecated use {@link #isCollapsed()} {@inheritDoc} */
@Override
@Deprecated
public boolean isHidden() {
return collapsible.isCollapsed();
}
/** {@inheritDoc} */
@Override
public boolean isCollapsed() {
return collapsible.isCollapsed();
}
/** {@inheritDoc} */
@Override
public TreeItem addHideListener(Collapsible.HideCompletedHandler handler) {
collapsible.addHideHandler(handler);
return this;
}
/** {@inheritDoc} */
@Override
public TreeItem removeHideListener(Collapsible.HideCompletedHandler handler) {
collapsible.removeHideHandler(handler);
return this;
}
/** {@inheritDoc} */
@Override
public TreeItem addShowListener(Collapsible.ShowCompletedHandler handler) {
collapsible.addShowHandler(handler);
return this;
}
/** {@inheritDoc} */
@Override
public TreeItem removeShowListener(Collapsible.ShowCompletedHandler handler) {
collapsible.removeShowHandler(handler);
return this;
}
/** {@inheritDoc} */
@Override
public HTMLLIElement element() {
return element;
}
/** {@inheritDoc} */
@Override
public TreeItem getActiveItem() {
return activeTreeItem;
}
/** {@inheritDoc} */
@Override
public Tree getTreeRoot() {
return parent.getTreeRoot();
}
/** {@inheritDoc} */
@Override
public Optional> getParent() {
if (parent instanceof TreeItem) {
return Optional.of((TreeItem) parent);
} else {
return Optional.empty();
}
}
/** {@inheritDoc} */
@Override
public void setActiveItem(TreeItem activeItem) {
setActiveItem(activeItem, false);
}
/** {@inheritDoc} */
@Override
public void setActiveItem(TreeItem activeItem, boolean silent) {
if (nonNull(activeItem)) {
if (nonNull(this.activeTreeItem) && !this.activeTreeItem.equals(activeItem)) {
this.activeTreeItem.deactivate();
}
this.activeTreeItem = activeItem;
this.activeTreeItem.activate();
parent.setActiveItem(this, true);
if (!silent) {
getTreeRoot().onTreeItemClicked(activeItem);
}
}
}
/** @return A list of tree items representing the path for this item */
public List> getPath() {
List> items = new ArrayList<>();
items.add(this);
Optional> parent = getParent();
while (parent.isPresent()) {
items.add(parent.get());
parent = parent.get().getParent();
}
Collections.reverse(items);
return items;
}
/** @return A list of values representing the path for this item */
public List getPathValues() {
List values = new ArrayList<>();
values.add(this.getValue());
Optional> parent = getParent();
while (parent.isPresent()) {
values.add(parent.get().getValue());
parent = parent.get().getParent();
}
Collections.reverse(values);
return values;
}
/** {@inheritDoc} */
@Override
public void activate() {
activate(false);
}
/** {@inheritDoc} */
@Override
public void activate(boolean activateParent) {
Style.of(element()).addCss("active");
if (isNull(expandIcon) || collapsible.isCollapsed() || !isParent()) {
replaceIcon(this.activeIcon);
}
if (activateParent && nonNull(parent)) {
parent.setActiveItem(this);
}
}
private void replaceIcon(BaseIcon> newIcon) {
if (nonNull(newIcon)) {
if (nonNull(icon)) {
icon.remove();
}
anchorElement.insertFirst(newIcon);
this.icon = newIcon;
}
}
/** {@inheritDoc} */
@Override
public void deactivate() {
Style.of(element()).removeCss("active");
if (isNull(expandIcon) || collapsible.isCollapsed() || !isParent()) {
restoreIcon();
}
if (isParent()) {
subItems.forEach(TreeItem::deactivate);
if (getTreeRoot().isAutoCollapse()) {
collapsible.hide();
}
}
}
private void restoreIcon() {
if (nonNull(originalIcon)) {
icon.remove();
anchorElement.insertFirst(originalIcon);
this.icon = originalIcon;
} else {
if (nonNull(icon)) {
icon.remove();
}
}
}
/** {@inheritDoc} */
@Override
public HTMLAnchorElement getClickableElement() {
return anchorElement.element();
}
/** {@inheritDoc} */
public TreeItem addClickListener(EventListener listener) {
getClickableElement().addEventListener(EventType.click.getName(), listener);
return this;
}
/**
* Sets the icon of the item
*
* @param icon the new {@link BaseIcon}
* @return same instance
*/
public TreeItem setIcon(BaseIcon> icon) {
this.icon = icon;
this.originalIcon = icon.copy();
if (icon.element().style.visibility.equals("hidden")) {
this.originalIcon.setCssProperty("visibility", "hidden");
}
this.originalIcon.addClickListener(
evt -> {
if (ToggleTarget.ICON.equals(this.toggleTarget)) {
evt.stopPropagation();
toggle();
}
activateItem();
});
return this;
}
/**
* Sets the icon that will be shown when the item is active
*
* @param activeIcon the {@link BaseIcon}
* @return same instance
*/
public TreeItem setActiveIcon(BaseIcon> activeIcon) {
this.activeIcon = activeIcon;
return this;
}
/**
* Sets the expand icon
*
* @param expandIcon the {@link BaseIcon}
* @return same instance
*/
public TreeItem setExpandIcon(BaseIcon> expandIcon) {
this.expandIcon = expandIcon;
return this;
}
boolean isParent() {
return !subItems.isEmpty();
}
void setParent(ParentTreeItem> parentMenu) {
this.parent = parentMenu;
}
/** @return the title of the item */
public String getTitle() {
return title;
}
/**
* Filter this item based on the search token
*
* @param searchToken the search token
* @return true if this item should be shown, false otherwise
*/
public boolean filter(String searchToken) {
boolean found;
if (isNull(this.originalState)) {
this.originalState = new OriginalState(collapsible.isExpanded());
}
if (isParent()) {
found = getFilter().filter(this, searchToken) | filterChildren(searchToken);
} else {
found = getFilter().filter(this, searchToken);
}
if (found) {
Style.of(element).removeCssProperty("display");
if (isParent() && isAutoExpandFound() && collapsible.isCollapsed()) {
collapsible.show();
}
return true;
} else {
Style.of(element).setDisplay("none");
return false;
}
}
/** {@inheritDoc} */
@Override
public boolean isAutoExpandFound() {
return parent.isAutoExpandFound();
}
/** Clears the filter applied */
public void clearFilter() {
if (nonNull(originalState)) {
DomGlobal.requestAnimationFrame(
timestamp -> {
if (collapsible.isExpanded() != originalState.expanded) {
if (this.equals(this.getTreeRoot().getActiveItem())) {
collapsible.show();
} else {
collapsible.toggleDisplay(originalState.expanded);
}
}
this.originalState = null;
});
}
Style.of(element).removeCssProperty("display");
subItems.forEach(TreeItem::clearFilter);
}
/**
* Filters the children and make sure the filter is applied to all children
*
* @param searchToken the search token
* @return true of one of the children matches the search token, false otherwise
*/
public boolean filterChildren(String searchToken) {
// We use the noneMatch here instead of anyMatch to make sure we are looping all children
// instead of early exit on first matching one
return subItems.stream().filter(treeItem -> treeItem.filter(searchToken)).count() > 0;
}
/** Collapse all children */
public void collapseAll() {
if (isParent() && !collapsible.isCollapsed()) {
hide();
subItems.forEach(TreeItem::collapseAll);
}
}
/** Expand all children */
public void expandAll() {
if (isParent() && collapsible.isCollapsed()) {
show();
subItems.forEach(TreeItem::expandAll);
}
}
/**
* Sets the level of this item
*
* @param level the new level
* @return same instance
*/
public TreeItem setLevel(int level) {
this.level = level;
updateLevelPadding();
if (isParent()) {
subItems.forEach(treeItem -> treeItem.setLevel(level + 1));
}
return this;
}
/**
* Sets the level padding of this item
*
* @param levelPadding the new level padding
* @return same instance
*/
public TreeItem setLevelPadding(int levelPadding) {
this.levelPadding = levelPadding;
updateLevelPadding();
if (isParent()) {
subItems.forEach(treeItem -> treeItem.setLevelPadding(levelPadding));
}
return this;
}
private void updateLevelPadding() {
anchorElement.style().setPaddingLeft(px.of(level * levelPadding));
}
/** {@inheritDoc} */
@Override
public HTMLElement getWavesElement() {
return anchorElement.element();
}
/** @return true if this item does not have children, false otherwise */
public boolean isLeaf() {
return subItems.isEmpty();
}
/** @return the list of all sub {@link TreeItem} */
@Override
public List> getSubItems() {
return new ArrayList<>(subItems);
}
/** Selects this item, the item will be shown and activated */
public void select() {
this.show(true).activate(true);
}
/** @return the value of the item */
public T getValue() {
return value;
}
/**
* Sets the value of the item
*
* @param value the value
*/
public void setValue(T value) {
this.value = value;
}
/** {@inheritDoc} */
@Override
public void removeItem(TreeItem item) {
subItems.remove(item);
item.remove();
}
/** {@inheritDoc} */
@Override
public TreeItem remove() {
if (parent.getSubItems().contains(this)) {
parent.removeItem(this);
if (parent.getSubItems().isEmpty() && parent instanceof TreeItem) {
((TreeItem) parent).style().removeCss("tree-item-parent");
}
}
return super.remove();
}
/**
* Sets the content indicator for this item
*
* @param indicatorContent a {@link Node}
* @return same instance
*/
public TreeItem setIndicatorContent(Node indicatorContent) {
indicatorContainer.clearElement();
if (nonNull(indicatorContent)) {
indicatorContainer.appendChild(indicatorContent);
}
return this;
}
/**
* Sets the content indicator for this item
*
* @param element a {@link IsElement}
* @return same instance
*/
public TreeItem setIndicatorContent(IsElement> element) {
setIndicatorContent(element.element());
return this;
}
/**
* Clears the content indicator
*
* @return same instance
*/
public TreeItem clearIndicator() {
indicatorContainer.clearElement();
return this;
}
/** {@inheritDoc} */
@Override
public TreeItemFilter> getFilter() {
return parent.getFilter();
}
/** {@inheritDoc} */
@Override
public Collapsible getCollapsible() {
return collapsible;
}
/** @return the content indicator container */
public DominoElement getIndicatorContainer() {
return indicatorContainer;
}
/** @return The {@link HTMLElement} that contains the title of this TreeItem */
public DominoElement getTitleElement() {
return DominoElement.of(titleElement);
}
/**
* Change the title of a TreeItem, If the TreeItem was created without a value and the title is
* used as a value then it will not change when the title is changed to change the value a call to
* {@link #setValue(T)} should be called
*
* @param title String title to set
* @return same TreeItem instance
*/
public TreeItem setTitle(String title) {
this.title = title;
getTitleElement().setTextContent(title);
return this;
}
public HTMLUListElement getChildrenContainer() {
return childrenContainer;
}
private static class OriginalState {
private boolean expanded;
public OriginalState(boolean expanded) {
this.expanded = expanded;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy