All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dominokit.domino.ui.datatable.plugins.TreeGridPlugin 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.datatable.plugins;

import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;

import elemental2.dom.HTMLDivElement;
import elemental2.dom.HTMLElement;
import elemental2.dom.HTMLTableCellElement;
import elemental2.dom.Node;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.dominokit.domino.ui.datatable.CellRenderer;
import org.dominokit.domino.ui.datatable.ColumnConfig;
import org.dominokit.domino.ui.datatable.DataTable;
import org.dominokit.domino.ui.datatable.RowCell;
import org.dominokit.domino.ui.datatable.TableRow;
import org.dominokit.domino.ui.datatable.events.DataSortEvent;
import org.dominokit.domino.ui.datatable.events.SearchClearedEvent;
import org.dominokit.domino.ui.datatable.events.SearchEvent;
import org.dominokit.domino.ui.datatable.events.SortEvent;
import org.dominokit.domino.ui.datatable.events.TableDataUpdatedEvent;
import org.dominokit.domino.ui.datatable.events.TableEvent;
import org.dominokit.domino.ui.datatable.events.TablePageChangeEvent;
import org.dominokit.domino.ui.grid.flex.FlexItem;
import org.dominokit.domino.ui.icons.BaseIcon;
import org.dominokit.domino.ui.icons.Icons;
import org.dominokit.domino.ui.style.Unit;
import org.dominokit.domino.ui.utils.DominoElement;
import org.dominokit.domino.ui.utils.TextNode;

public class TreeGridPlugin implements DataTablePlugin {

  public static final String TREE_GRID_ROW_LEVEL = "tree-grid-row-level";
  public static final String TREE_GRID_ROW_SUBITEMS = "tree-grid-row-sub-items";
  public static final String TREE_GRID_ROW_TOGGLE_ICON = "tree-grid-row-toggle-icon";
  public static final String TREE_GRID_EXPAND_COLLAPSE = "plugin-utility-column";
  public static final int DEFAULT_INDENT = 20;
  public static final int BASE_PADDING = 10;
  public static final String ICON_ORDER = "10";

  private final SubItemsProvider subItemsProvider;
  private ParentRowCellsSupplier parentRowCellsSupplier;
  private Supplier> expandIconSupplier = Icons.ALL::menu_right_mdi;
  private Supplier> collapseIconSupplier = Icons.ALL::menu_down_mdi;
  private Supplier> leafIconSupplier = Icons.ALL::circle_medium_mdi;
  private Function, Node> indentColumnElementSupplier = tableRow -> TextNode.empty();
  private int indent = DEFAULT_INDENT;
  private BaseIcon headerIcon;
  private int expandedCount = 0;
  private DataTable dataTable;

  public TreeGridPlugin(SubItemsProvider subItemsProvider) {
    this.subItemsProvider = subItemsProvider;
  }

  /** {@inheritDoc} */
  @Override
  public void init(DataTable dataTable) {
    this.dataTable = dataTable;
  }

  /** {@inheritDoc} */
  @Override
  public boolean requiresUtilityColumn() {
    return true;
  }

  /**
   * If the row has children it will expand the row, and based on recursive value it might also
   * expand its children sub-children
   *
   * @param row {@link TableRow} to be expanded
   * @param recursive boolean, if true will recursively expand the row children
   */
  public final void expandRow(TableRow row, boolean recursive) {
    expand(row, recursive);
    if (row.isRoot()) {
      increment();
    }
  }

  /**
   * If the row has children it will expand the row and recursively expand the row children
   *
   * @param row {@link TableRow} to be expanded
   */
  public final void expandRow(TableRow row) {
    expandRow(row, true);
  }

  /**
   * Expand all table rows, and based on recursive value it might also recursively expand all
   * children
   *
   * @param recursive boolean, if true will recursively expand the row children
   */
  public final void expandAllRows(boolean recursive) {
    dataTable.getRows().forEach(tableRow -> expandRow(tableRow, recursive));
  }

  /**
   * If the row has children it will collapse the row.
   *
   * @param row {@link TableRow} to be collapsed
   */
  public final void collapseRow(TableRow row) {
    collapse(row);
  }

  /** Collapse all table row */
  public final void collapseAllRows() {
    dataTable.getRows().forEach(this::collapseRow);
  }

  private void expand(TableRow row, boolean recursive) {
    row.show();
    if (recursive) {
      TreeGridRowToggleIcon treeGridRowToggleIcon = row.getMetaObject(TREE_GRID_ROW_TOGGLE_ICON);
      if (!treeGridRowToggleIcon.icon.isToggled()) {
        treeGridRowToggleIcon.icon.toggleIcon();
      }
      for (TableRow child : row.getChildren()) {
        expand(child, false);
      }
    }
  }

  private void collapse(TableRow row) {
    TreeGridRowToggleIcon treeGridRowToggleIcon = row.getMetaObject(TREE_GRID_ROW_TOGGLE_ICON);
    if (treeGridRowToggleIcon.icon.isToggled()) {
      treeGridRowToggleIcon.icon.toggleIcon();
    }
    for (TableRow child : row.getChildren()) {
      child.hide();
      collapse(child);
    }
    if (row.isRoot()) {
      decrement();
    }
  }

  private void increment() {
    expandedCount++;
    if (!headerIcon.isToggled()) {
      headerIcon.toggleIcon();
    }
  }

  private void decrement() {
    expandedCount--;
    if (expandedCount == 0 && headerIcon.isToggled()) {
      headerIcon.toggleIcon();
    }
  }

  private DominoElement getRowCellElement(TableRow subRow) {
    return DominoElement.of(
        subRow
            .getRowCells()
            .get(TreeGridPlugin.TREE_GRID_EXPAND_COLLAPSE)
            .getCellInfo()
            .getElement());
  }

  /** Adds the expand/collapse/leaf icons to the plugins utility columns cells {@inheritDoc} */
  @Override
  public Optional> getUtilityElements(
      DataTable dataTable, CellRenderer.CellInfo cellInfo) {
    List elements = new ArrayList<>();
    getSubRecords(
        cellInfo.getTableRow(),
        items -> {
          TableRow tableRow = cellInfo.getTableRow();
          BaseIcon icon;
          if (!isParent(items)) {
            icon = leafIconSupplier.get().css("dt-tree-grid-leaf");
          } else {
            icon = expandIconSupplier.get().setToggleIcon(collapseIconSupplier.get()).clickable();
            icon.addClickListener(
                evt -> {
                  if (icon.isToggled()) {
                    collapse(tableRow);
                  } else {
                    expandRow(tableRow);
                  }
                  evt.stopPropagation();
                });
          }
          icon.setAttribute("order", ICON_ORDER);
          DominoElement title =
              DominoElement.div()
                  .setAttribute("order", "100")
                  .appendChild(indentColumnElementSupplier.apply(tableRow));
          tableRow.addMetaObject(new TreeGridRowToggleIcon(icon));
          elements.add(icon.element());
          elements.add(title.element());
        });

    return Optional.of(elements);
  }

  private void getSubRecords(TableRow tableRow, Consumer>> consumer) {
    SubItemMetaObject metaObject = tableRow.getMetaObject(TREE_GRID_ROW_SUBITEMS);
    if (nonNull(metaObject)) {
      consumer.accept(metaObject.subItems);
    } else {
      subItemsProvider.get(tableRow.getRecord(), consumer);
    }
  }

  /** Adds the Expand all/collpase all to the plugins utility column header {@inheritDoc} */
  @Override
  public void onHeaderAdded(DataTable dataTable, ColumnConfig column) {
    if (column.isUtilityColumn()) {
      BaseIcon baseIcon =
          expandIconSupplier.get().setToggleIcon(collapseIconSupplier.get()).clickable();
      baseIcon.addClickListener(
          evt -> {
            if (baseIcon.isToggled()) {
              collapseAllRows();
            } else {
              expandAllRows(true);
            }
            evt.stopPropagation();
          });
      headerIcon = baseIcon;
      column.getHeaderLayout().appendChild(FlexItem.create().setOrder(10).appendChild(baseIcon));
    }
  }

  /** {@inheritDoc} */
  @Override
  public void onBeforeAddRow(DataTable dataTable, TableRow tableRow) {
    if (nonNull(parentRowCellsSupplier)) {
      getSubRecords(
          tableRow,
          items -> {
            if (isParent(items)) {
              tableRow.setRowRenderer(new TreeGridRowRenderer());
            }
          });
    }
  }

  /** {@inheritDoc} */
  @Override
  public void onRowAdded(DataTable dataTable, TableRow tableRow) {
    getSubRecords(
        tableRow,
        itemsOptional -> {
          if (!isParent(itemsOptional)) {
            return;
          }
          List items = new ArrayList<>(itemsOptional.get());
          List> subRows = new ArrayList<>();
          for (int i = 0; i < items.size(); i++) {
            TableRow subRow = new TableRow<>(items.get(i), i, dataTable);
            subRow.hide();
            TreeGridRowLevel treeGridRowLevel =
                Optional.ofNullable(tableRow.getMetaObject(TREE_GRID_ROW_LEVEL))
                    .map(o -> (TreeGridRowLevel) o)
                    .orElse(new TreeGridRowLevel(1));
            tableRow.addMetaObject(treeGridRowLevel);
            subRow.addMetaObject(new TreeGridRowLevel(treeGridRowLevel.level + 1));
            dataTable
                .getTableConfig()
                .getPlugins()
                .forEach(plugin -> plugin.onBeforeAddRow(dataTable, subRow));
            dataTable.getTableConfig().drawRecord(dataTable, subRow);
            dataTable.getRows().add(subRow);
            subRow.setParent(tableRow);
            subRows.add(subRow);
            getRowCellElement(subRow)
                .setPaddingLeft(Unit.px.of(treeGridRowLevel.level * indent + BASE_PADDING));
          }
          tableRow.setChildren(subRows);
        });
  }

  /** {@inheritDoc} */
  @Override
  public void handleEvent(TableEvent event) {
    switch (event.getType()) {
      case SortEvent.SORT_EVENT:
      case DataSortEvent.EVENT:
      case SearchEvent.SEARCH_EVENT:
      case SearchClearedEvent.SEARCH_EVENT_CLEARED:
      case TableDataUpdatedEvent.DATA_UPDATED:
      case TablePageChangeEvent.PAGINATION_EVENT:
        if (headerIcon.isToggled()) {
          headerIcon.toggleIcon();
        }
        expandedCount = 0;
        break;
    }
  }

  /**
   * Set a supplier that provides cells to be rendered in a parent row cells, this can be used to
   * provide a custom UI for parent rows
   *
   * @param parentRowCellsSupplier {@link ParentRowCellsSupplier}
   * @return Same plugin instance
   */
  public TreeGridPlugin setParentRowCellsSupplier(
      ParentRowCellsSupplier parentRowCellsSupplier) {
    this.parentRowCellsSupplier = parentRowCellsSupplier;
    return this;
  }

  /**
   * Sets a supplier for a custom expand icon instead of the default one
   *
   * @param expandIconSupplier {@link Supplier} of {@link BaseIcon}
   * @return Same plugin instance
   */
  public TreeGridPlugin setExpandIconSupplier(Supplier> expandIconSupplier) {
    if (isNull(expandIconSupplier)) {
      this.expandIconSupplier = () -> Icons.ALL.plus_mdi().size18();
    } else {
      this.expandIconSupplier = expandIconSupplier;
    }
    return this;
  }

  /**
   * Sets a supplier for a custom collapse icon instead of the default one
   *
   * @param collapseIconSupplier {@link Supplier} of {@link BaseIcon}
   * @return Same plugin instance
   */
  public TreeGridPlugin setCollapseIconSupplier(Supplier> collapseIconSupplier) {
    if (isNull(collapseIconSupplier)) {
      this.collapseIconSupplier = () -> Icons.ALL.minus_mdi().size18();
    } else {
      this.collapseIconSupplier = collapseIconSupplier;
    }
    return this;
  }

  /**
   * Sets a supplier for a custom leaf row icon instead of the default one
   *
   * @param leafIconSupplier {@link Supplier} of {@link BaseIcon}
   * @return Same plugin instance
   */
  public TreeGridPlugin setLeafIconSupplier(Supplier> leafIconSupplier) {
    if (isNull(leafIconSupplier)) {
      this.leafIconSupplier = () -> Icons.ALL.circle_medium_mdi().size18();
    } else {
      this.leafIconSupplier = leafIconSupplier;
    }
    return this;
  }

  /**
   * Sets indent value to be added for each tree gird level
   *
   * @param indent int
   * @return Same plugin instance
   */
  public TreeGridPlugin setIndent(int indent) {
    if (indent < 0) {
      this.indent = DEFAULT_INDENT;
    } else {
      this.indent = indent;
    }
    return this;
  }

  /**
   * Sets a supplier of elements to be appended to the tree grid indent column as part of the
   * utility columns cells
   *
   * @param indentColumnElementSupplier {@link Function} that takes a {@link TableRow} and return a
   *     {@link Node}
   * @return same plugin instance
   */
  public TreeGridPlugin setIndentColumnElementSupplier(
      Function, Node> indentColumnElementSupplier) {
    if (isNull(indentColumnElementSupplier)) {
      this.indentColumnElementSupplier = tableRow -> TextNode.empty();
    } else {
      this.indentColumnElementSupplier = indentColumnElementSupplier;
    }
    return this;
  }

  private boolean isParent(Optional> items) {
    return items.isPresent() && !items.get().isEmpty();
  }

  /**
   * Functional interface to provide the cells to be rendered in a parent row
   *
   * @param  Type of the table records
   */
  @FunctionalInterface
  public interface ParentRowCellsSupplier {
    List> get(DataTable dataTable, TableRow tableRow);
  }

  /**
   * A functional interface to supply record children
   *
   * @param  Type of table records.
   */
  @FunctionalInterface
  public interface SubItemsProvider {
    void get(T parent, Consumer>> itemsConsumer);
  }

  private static class TreeGridRowLevel implements TableRow.RowMetaObject {
    private final int level;

    public TreeGridRowLevel(int level) {
      this.level = level;
    }

    @Override
    public String getKey() {
      return TREE_GRID_ROW_LEVEL;
    }
  }

  private static class SubItemMetaObject implements TableRow.RowMetaObject {
    private final Optional> subItems;

    public SubItemMetaObject(Optional> subItems) {
      this.subItems = subItems;
    }

    @Override
    public String getKey() {
      return TREE_GRID_ROW_SUBITEMS;
    }
  }

  private static class TreeGridRowToggleIcon implements TableRow.RowMetaObject {
    private final BaseIcon icon;

    public TreeGridRowToggleIcon(BaseIcon icon) {
      this.icon = icon;
    }

    @Override
    public String getKey() {
      return TREE_GRID_ROW_TOGGLE_ICON;
    }
  }

  private class TreeGridRowRenderer implements TableRow.RowRenderer {
    @Override
    public void render(DataTable dataTable, TableRow tableRow) {
      List> columns = dataTable.getTableConfig().getColumns();
      columns.stream().filter(ColumnConfig::isPluginColumn).forEach(tableRow::renderCell);

      List> rowCells = parentRowCellsSupplier.get(dataTable, tableRow);
      rowCells.forEach(
          rowCell -> {
            tableRow.addCell(rowCell);
            tableRow.element().appendChild(rowCell.getCellInfo().getElement());
          });
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy