org.dominokit.domino.ui.datatable.DataTable 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;
import static java.util.Objects.isNull;
import static java.util.Objects.nonNull;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TABLE;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TABLE_BORDERED;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TABLE_CONDENSED;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TABLE_FIXED;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TABLE_HOVER;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TABLE_RESPONSIVE;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TABLE_ROW_FILTERED;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TABLE_STRIPED;
import static org.dominokit.domino.ui.datatable.DataTableStyles.TBODY_FIXED;
import static org.dominokit.domino.ui.datatable.DataTableStyles.THEAD_FIXED;
import static org.dominokit.domino.ui.style.Unit.px;
import static org.jboss.elemento.Elements.div;
import static org.jboss.elemento.Elements.table;
import static org.jboss.elemento.Elements.tbody;
import static org.jboss.elemento.Elements.thead;
import elemental2.dom.DomGlobal;
import elemental2.dom.HTMLDivElement;
import elemental2.dom.HTMLTableElement;
import elemental2.dom.HTMLTableSectionElement;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.dominokit.domino.ui.datatable.events.DataSortEvent;
import org.dominokit.domino.ui.datatable.events.OnBeforeDataChangeEvent;
import org.dominokit.domino.ui.datatable.events.TableDataUpdatedEvent;
import org.dominokit.domino.ui.datatable.events.TableEvent;
import org.dominokit.domino.ui.datatable.events.TableEventListener;
import org.dominokit.domino.ui.datatable.model.SearchContext;
import org.dominokit.domino.ui.datatable.store.DataStore;
import org.dominokit.domino.ui.utils.BaseDominoElement;
import org.dominokit.domino.ui.utils.DominoElement;
import org.dominokit.domino.ui.utils.HasSelectionSupport;
import org.jboss.elemento.EventType;
/**
* The data table component
*
* @param the type of the data table records
*/
public class DataTable extends BaseDominoElement>
implements HasSelectionSupport> {
/** Use this constant to register a table event listener that listen to all events */
public static final String ANY = "*";
/** Use this constant as flag value to check if a row in the data tables have been filtered out */
public static final String DATA_TABLE_ROW_FILTERED = "data-table-row-filtered";
private final DataStore dataStore;
private DominoElement root = DominoElement.of(div()).css(TABLE_RESPONSIVE);
private DominoElement tableElement =
DominoElement.of(table()).css(TABLE, TABLE_HOVER, TABLE_STRIPED);
private TableConfig tableConfig;
private DominoElement tbody = DominoElement.of(tbody());
private DominoElement thead = DominoElement.of(thead());
private List data = new ArrayList<>();
private boolean selectable = true;
private List> tableRows = new ArrayList<>();
private List> selectionChangeListeners = new ArrayList<>();
private boolean condensed = false;
private boolean hoverable = true;
private boolean striped = true;
private boolean bordered = false;
private Map> events = new HashMap<>();
private final SearchContext searchContext = new SearchContext<>(this);
private double scrollBarWidth = -1;
/**
* Creates a new data table instance
*
* @param tableConfig the {@link TableConfig}
* @param dataStore the {@link DataStore}
*/
public DataTable(TableConfig tableConfig, DataStore dataStore) {
super.init(this);
this.tableConfig = tableConfig;
this.events.put(ANY, new ArrayList<>());
this.dataStore = dataStore;
this.addTableEventListener(ANY, dataStore);
this.dataStore.onDataChanged(
dataChangedEvent -> {
fireTableEvent(
new OnBeforeDataChangeEvent<>(
this.data, dataChangedEvent.getTotalCount(), dataChangedEvent.isAppend()));
if (dataChangedEvent.getSortDir().isPresent()
&& dataChangedEvent.getSortColumn().isPresent()) {
fireTableEvent(
new DataSortEvent(
dataChangedEvent.getSortDir().get(), dataChangedEvent.getSortColumn().get()));
}
if (dataChangedEvent.isAppend()) {
appendData(dataChangedEvent.getNewData());
} else {
setData(dataChangedEvent.getNewData());
}
fireTableEvent(new TableDataUpdatedEvent<>(this.data, dataChangedEvent.getTotalCount()));
});
init();
}
public DataStore getDataStore() {
return dataStore;
}
private DataTable init() {
tableConfig
.getPlugins()
.forEach(
plugin -> {
DataTable.this.addTableEventListener("*", plugin);
plugin.init(DataTable.this);
plugin.onBeforeAddTable(DataTable.this);
});
tableConfig.onBeforeHeaders(this);
tableConfig.drawHeaders(this, thead);
tableConfig.onAfterHeaders(this);
tableElement.appendChild(tbody);
tableConfig.getPlugins().forEach(plugin -> plugin.onBodyAdded(DataTable.this));
root.appendChild(tableElement);
tableConfig.getPlugins().forEach(plugin -> plugin.onAfterAddTable(DataTable.this));
if (!tableConfig.isLazyLoad()) {
this.dataStore.load();
}
if (tableConfig.isFixed()) {
root.addCss(TABLE_FIXED);
thead.addCss(THEAD_FIXED);
tbody.addCss(TBODY_FIXED).setMaxHeight(tableConfig.getFixedBodyHeight());
tableElement.addEventListener(EventType.scroll, e -> updateTableWidth());
DomGlobal.window.addEventListener(
EventType.resize.getName(),
e -> {
this.scrollBarWidth = -1;
updateTableWidth();
});
}
onResize(
(target, observer, entries) -> {
DomGlobal.requestAnimationFrame(
timestamp -> {
if (isNull(entries) || entries.length <= 0) {
return;
}
updateTableWidth();
});
});
return this;
}
private void updateTableWidth() {
final long w =
tableElement.element().offsetWidth + Math.round(tableElement.element().scrollLeft);
thead.setWidth(px.of(w - 2));
tbody.setWidth(px.of(w - 2));
if (tableConfig.isFixed()) {
updateHeadWidth(false);
}
}
/** Force loading the data into the table */
public void load() {
this.dataStore.load();
}
/**
* Set the table data
*
* @param data {@link List} of T
*/
public void setData(List data) {
this.data = data;
tableRows.clear();
tbody.clearElement();
if (nonNull(data) && !data.isEmpty()) {
addRows(data, 0);
}
updateHeadWidth(true);
}
private void updateHeadWidth(boolean scrollTop) {
DomGlobal.requestAnimationFrame(
timestamp -> {
DomGlobal.setTimeout(
p0 -> {
if (hasVScrollBar()) {
if (scrollTop) {
tbody.element().scrollTop = 0.0;
}
if (tableConfig.isFixed()) {
thead.setWidth(px.of(tbody.element().offsetWidth - getScrollWidth()));
tbody.setWidth(px.of(tbody.element().offsetWidth));
}
}
});
});
}
private boolean hasVScrollBar() {
return tbody.element().scrollHeight > tbody.element().clientHeight;
}
private double getScrollWidth() {
if (scrollBarWidth == -1) {
DominoElement outer =
DominoElement.div()
.setTop("-1000px")
.setLeft("-1000px")
.setWidth("100px")
.setHeight("50px")
.setOverFlow("hidden")
.setCssProperty("-ms-overflow-style", "hidden");
DominoElement inner = DominoElement.div().setHeight("200px");
outer.appendChild(inner);
DominoElement.body().appendChild(outer);
double noScrollWidth = inner.element().offsetWidth;
outer.setOverFlow("auto").setCssProperty("-ms-overflow-style", "scrollbar");
double widthWithScroll = inner.element().clientWidth;
outer.remove();
scrollBarWidth = noScrollWidth - widthWithScroll;
}
return scrollBarWidth;
}
/**
* Appends more records to the current data list of the table
*
* @param newData {@link List} of T
*/
public void appendData(List newData) {
if (nonNull(this.data)) {
addRows(newData, this.data.size());
this.data.addAll(newData);
} else {
setData(newData);
}
}
private void addRows(List data, int initialIndex) {
tableConfig.getColumns().forEach(ColumnConfig::clearShowHideListeners);
for (int index = 0; index < data.size(); index++) {
TableRow tableRow = new TableRow<>(data.get(index), initialIndex + index, this);
tableConfig.getPlugins().forEach(plugin -> plugin.onBeforeAddRow(DataTable.this, tableRow));
tableConfig.drawRecord(DataTable.this, tableRow);
tableRows.add(tableRow);
}
tableConfig.getPlugins().forEach(plugin -> plugin.onAllRowsAdded(DataTable.this));
}
/** @return the {@link Collection} of T that is the current data in the table */
public Collection getData() {
return data;
}
/**
* Increases the height of the data table rows
*
* @return same DataTable instance
*/
public DataTable uncondense() {
tableElement.removeCss(TABLE_CONDENSED);
this.condensed = false;
return this;
}
/**
* Decreases the height of the data table rows
*
* @return same DataTable instance
*/
public DataTable condense() {
tableElement.addCss(TABLE_CONDENSED);
this.condensed = true;
return this;
}
/**
* removes the hover effect from data table rows
*
* @return same DataTable instance
*/
public DataTable noHover() {
tableElement.removeCss(TABLE_HOVER);
this.hoverable = false;
return this;
}
/**
* Adds the hover effect to the data table rows
*
* @return same DataTable instance
*/
public DataTable hovered() {
noHover();
tableElement.addCss(TABLE_HOVER);
this.hoverable = true;
return this;
}
/**
* Remove the borders from the data table rows
*
* @return same DataTable instance
*/
public DataTable noBorder() {
tableElement.removeCss(TABLE_BORDERED);
this.bordered = false;
return this;
}
/**
* Adds borders from the data table rows
*
* @return same DataTable instance
*/
public DataTable bordered() {
noBorder();
tableElement.addCss(TABLE_BORDERED);
this.bordered = true;
return this;
}
/**
* Remove the background alternation from the data table rows
*
* @return same DataTable instance
*/
public DataTable noStripes() {
tableElement.removeCss(TABLE_STRIPED);
this.striped = false;
return this;
}
/**
* Adds background alternation from the data table rows
*
* @return same DataTable instance
*/
public DataTable striped() {
noStripes();
tableElement.addCss(TABLE_STRIPED);
this.striped = true;
return this;
}
/**
* Render all table rows in editable mode
*
* @return same DataTable instance
*/
public DataTable edit() {
getRows().forEach(TableRow::edit);
return this;
}
/**
* Saves all editable table rows changes
*
* @return same DataTable instance
*/
public DataTable save() {
getRows().forEach(TableRow::save);
return this;
}
/**
* Cancel editing of all table rows
*
* @return same DataTable instance
*/
public DataTable cancelEditing() {
getRows().forEach(TableRow::cancelEditing);
return this;
}
/** @return the {@link HTMLTableElement} wrapped as {@link DominoElement} */
public DominoElement tableElement() {
return tableElement;
}
/** @return the {@link HTMLTableSectionElement} -tbody- wrapped as {@link DominoElement} */
public DominoElement bodyElement() {
return tbody;
}
/** @return the {@link HTMLTableSectionElement} -thead- wrapped as {@link DominoElement} */
public DominoElement headerElement() {
return thead;
}
/** @return the applied {@link TableConfig} of this table */
public TableConfig getTableConfig() {
return tableConfig;
}
/** @return boolean */
public boolean isCondensed() {
return condensed;
}
/** @return boolean */
public boolean isHoverable() {
return hoverable;
}
/** @return boolean */
public boolean isStriped() {
return striped;
}
/** @return boolean */
public boolean isBordered() {
return bordered;
}
/**
* Immediately filter the current table rows using the the specified filter
*
* @param rowFilter {@link LocalRowFilter}
*/
public void filterRows(LocalRowFilter rowFilter) {
tableRows.forEach(
tableRow -> {
if (rowFilter.filter(tableRow)) {
tableRow.removeCss(TABLE_ROW_FILTERED);
tableRow.removeFlag(DATA_TABLE_ROW_FILTERED);
tableRow.fireUpdate();
} else {
tableRow.addCss(TABLE_ROW_FILTERED);
tableRow.setFlag(DATA_TABLE_ROW_FILTERED, "true");
tableRow.deselect();
tableRow.fireUpdate();
}
});
}
/** Clear all filtration applied using {@link #filterRows(LocalRowFilter)} */
public void clearRowFilters() {
tableRows.stream()
.filter(tableRow -> nonNull(tableRow.getFlag(DATA_TABLE_ROW_FILTERED)))
.forEach(
tableRow -> {
tableRow.removeCss(TABLE_ROW_FILTERED);
tableRow.removeFlag(DATA_TABLE_ROW_FILTERED);
tableRow.fireUpdate();
});
}
/** {@inheritDoc} */
@Override
public HTMLDivElement element() {
return root.element();
}
/** @return */
@Override
public List> getSelectedItems() {
return tableRows.stream().filter(TableRow::isSelected).collect(Collectors.toList());
}
/** @return a {@link List} of the currently selected records including a row selected children */
public List getSelectedRecords() {
return tableRows.stream()
.filter(TableRow::isSelected)
.map(TableRow::getRecord)
.collect(Collectors.toList());
}
/** @return a {@link List} of {@link TableRow}s including the child rows */
@Override
@Deprecated
public List> getItems() {
return getRows();
}
/** @return a {@link List} of {@link TableRow}s including the child rows */
@Override
public List> getRows() {
return tableRows;
}
public List> getRootRows() {
return getRows().stream().filter(TableRow::isRoot).collect(Collectors.toList());
}
/** @return a {@link List} of {@link TableRow}s excluding the child rows */
public List getRecords() {
return getRows().stream()
.filter(TableRow::isRoot)
.map(TableRow::getRecord)
.collect(Collectors.toList());
}
/** @return a {@link List} of {@link TableRow}s that are being edited and still not saved */
public List getDirtyRecords() {
return getRows().stream().map(TableRow::getDirtyRecord).collect(Collectors.toList());
}
@Override
public void onSelectionChange(TableRow source) {
selectionChangeListeners.forEach(
selectionChangeListener ->
selectionChangeListener.onSelectionChanged(getSelectedItems(), getSelectedRecords()));
}
/** Select all table rows */
@Override
public void selectAll() {
selectAll((table, tableRow) -> true);
}
/** Select all table rows that match a condition */
public void selectAll(SelectionCondition selectionCondition) {
if (tableConfig.isMultiSelect() && !tableRows.isEmpty()) {
for (TableRow tableRow : tableRows) {
if (selectionCondition.isAllowSelection(this, tableRow)) {
tableRow.select();
}
}
onSelectionChange(tableRows.get(0));
}
}
/** Deselect all table rows */
@Override
public void deselectAll() {
deselectAll((table, tableRow) -> true);
}
/** Deselect all table rows that match a condition */
public void deselectAll(SelectionCondition selectionCondition) {
if (!tableRows.isEmpty()) {
for (TableRow tableRow : tableRows) {
if (tableRow.isSelected()) {
if (selectionCondition.isAllowSelection(this, tableRow)) {
tableRow.deselect();
}
}
}
onSelectionChange(tableRows.get(0));
}
}
/** {@inheritDoc} */
@Override
public boolean isSelectable() {
return this.selectable;
}
/**
* Add a listener to listen to data table selection changes
*
* @param selectionChangeListener {@link SelectionChangeListener}
*/
public void addSelectionListener(SelectionChangeListener selectionChangeListener) {
this.selectionChangeListeners.add(selectionChangeListener);
}
/** @param selectionChangeListener {@link SelectionChangeListener} */
public void removeSelectionListener(SelectionChangeListener selectionChangeListener) {
this.selectionChangeListeners.remove(selectionChangeListener);
}
/** @deprecated use {@link #addTableEventListener(String, TableEventListener)} */
@Deprecated
public void addTableEventListner(String type, TableEventListener listener) {
addTableEventListener(type, listener);
}
/**
* Adds a table event listener by event type
*
* @param type String type of the event
* @param listener {@link TableEventListener}
*/
public void addTableEventListener(String type, TableEventListener listener) {
if (!events.containsKey(type)) {
events.put(type, new ArrayList<>());
}
events.get(type).add(listener);
}
/**
* Removes a table event listener by event type
*
* @param type String type of the event
* @param listener {@link TableEventListener}
*/
public void removeTableListener(String type, TableEventListener listener) {
if (events.containsKey(type)) {
events.get(type).remove(listener);
}
}
/**
* Manually fire a table event
*
* @param tableEvent {@link TableEvent}
*/
public void fireTableEvent(TableEvent tableEvent) {
if (events.containsKey(tableEvent.getType())) {
events.get(tableEvent.getType()).forEach(listener -> listener.handleEvent(tableEvent));
}
events.get(ANY).forEach(listener -> listener.handleEvent(tableEvent));
}
/** @return the current {@link SearchContext} of the data table */
public SearchContext getSearchContext() {
return searchContext;
}
/**
* Listens to changes in the table rows selection
*
* @param the type of the data table records
*/
@FunctionalInterface
public interface SelectionChangeListener {
/**
* @param selectedTableRows {@link List} of {@link TableRow}s that has that are selected
* @param selectedRecords {@link List} of T records of the rows being selected
*/
void onSelectionChanged(List> selectedTableRows, List selectedRecords);
}
/**
* Use implement Table row filter
*
* @param the type of the data table records
*/
public interface LocalRowFilter {
/**
* @param tableRow {@link TableRow}
* @return boolean, true if the table row should be hidden else false.
*/
boolean filter(TableRow tableRow);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy