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.nonNull;
import static org.dominokit.domino.ui.utils.Domino.*;
import elemental2.dom.DomGlobal;
import elemental2.dom.EventListener;
import elemental2.dom.HTMLDivElement;
import elemental2.dom.Node;
import java.util.*;
import java.util.stream.Collectors;
import org.dominokit.domino.ui.IsElement;
import org.dominokit.domino.ui.datatable.events.*;
import org.dominokit.domino.ui.datatable.model.SearchContext;
import org.dominokit.domino.ui.datatable.store.DataStore;
import org.dominokit.domino.ui.elements.*;
import org.dominokit.domino.ui.events.EventType;
import org.dominokit.domino.ui.style.BooleanCssClass;
import org.dominokit.domino.ui.utils.*;
/**
* Represents a flexible and feature-rich data table for displaying and interacting with data. The
* DataTable provides features like column configurations, event listeners, row selection, row
* filtering, etc. for working with both tabular data and hierarchical data structures.
*
* @param the type of data to be displayed in the table
* @see BaseDominoElement
*/
public class DataTable extends BaseDominoElement>
implements HasSelectionSupport>,
HasSelectionListeners, TableRow, List>>,
DataTableStyles {
/** Constant used for wildcard event listeners. */
public static final String ANY = "*";
/** CSS class to indicate that a data table row is filtered. */
public static final String DATA_TABLE_ROW_FILTERED = "data-table-row-filtered";
/** Prefix used for parent selector CSS. */
private static final String PARENT_SELECTOR_PREFIX = "dui-dt-";
private final DataStore dataStore;
private DivElement root = div().addCss(dui_datatable_responsive);
private TableElement tableElement;
private TableConfig tableConfig;
private TBodyElement tbody = tbody().addCss(dui_datatable_body);
private THeadElement thead = thead().addCss(dui_datatable_thead);
private TFootElement tfoot = tfoot().addCss(dui_datatable_tfoot);
private List data = new ArrayList<>();
private boolean selectable = true;
private List> tableRows = new ArrayList<>();
private boolean selectionListenersPaused = false;
private Map> events = new HashMap<>();
private final SearchContext searchContext = new SearchContext<>(this);
private RemoveRowsHandler removeRecordsHandler = table -> table.bodyElement().clearElement();
private EventListener disableKeyboardListener =
evt -> {
if (isDisabled()) {
evt.preventDefault();
evt.stopPropagation();
}
};
private DynamicStyleSheet> dynamicStyleSheet;
private Set, ? super List>>>
selectionListeners = new HashSet<>();
private Set, ? super List>>>
deselectionListeners = new HashSet<>();
/**
* Constructs a DataTable with the provided table configuration and data store.
*
* @param tableConfig Configuration details of the table.
* @param dataStore Data storage handler to fetch and manipulate data for the table.
*/
public DataTable(TableConfig tableConfig, DataStore dataStore) {
tableElement = table().addCss(dui_datatable, dui_datatable_width_full);
super.init(this);
this.tableConfig = tableConfig;
this.events.put(ANY, new ArrayList<>());
this.dataStore = dataStore;
this.addTableEventListener(ANY, dataStore);
tableElement.setAttribute("dui-data-v-scroll", 0);
tableElement.setAttribute("dui-data-h-scroll", 0);
this.addEventListener(EventType.keydown.getName(), disableKeyboardListener, true);
tableElement.addEventListener(
"scroll",
evt -> {
double scrollTop = new Double(tableElement.element().scrollTop).intValue();
double scrollLeft = new Double(tableElement.element().scrollLeft).intValue();
tableElement.setAttribute("dui-data-v-scroll", scrollTop);
tableElement.setAttribute("dui-data-h-scroll", scrollLeft);
});
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()));
});
initDynamicStyleSheet();
init();
onAttached(
mutationRecord -> {
DomGlobal.setTimeout(
p0 -> {
getDynamicStyleSheet().flush();
},
0);
});
addCss(dui_datatable_hover, dui_datatable_striped);
}
/** Initializes dynamic style sheet configurations for the table columns. */
private void initDynamicStyleSheet() {
this.dynamicStyleSheet = new DynamicStyleSheet<>(PARENT_SELECTOR_PREFIX, this);
tableConfig
.getColumnsGrouped()
.forEach(
col -> {
col.applyAndOnSubColumns(
column -> {
ColumnCssRuleMeta columnCssRuleMeta =
ColumnCssRuleMeta.of(this.dynamicStyleSheet);
columnCssRuleMeta.addRule(
ColumnCssRuleMeta.DEFAULT_RULE,
"col-" + column.getName().replace(" ", "-"));
column.applyMeta(columnCssRuleMeta);
});
});
}
/**
* Returns the data store associated with this data table.
*
* @return the associated DataStore instance.
*/
public DataStore getDataStore() {
return dataStore;
}
/**
* Initializes and sets up the data table UI components, plugins, and listeners.
*
* @return the initialized DataTable instance.
*/
private DataTable init() {
tableConfig
.getPlugins()
.forEach(
plugin -> {
DataTable.this.addTableEventListener("*", plugin);
plugin.init(DataTable.this);
plugin.onBeforeAddTable(DataTable.this);
});
thead.clearElement();
if (tableConfig.isStickyHeader()) {
addCss(dui_datatable_sticky_header);
}
tableConfig.onBeforeHeaders(this);
tableConfig.drawHeaders(this, thead);
tableConfig.onAfterHeaders(this);
tableElement.appendChild(tbody);
tableConfig.getPlugins().forEach(plugin -> plugin.onBodyAdded(DataTable.this));
tableElement.appendChild(tfoot);
tableConfig.getPlugins().forEach(plugin -> plugin.onFooterAdded(DataTable.this));
appendChild(tableElement);
tableConfig.getPlugins().forEach(plugin -> plugin.onAfterAddTable(DataTable.this));
if (!tableConfig.isLazyLoad()) {
this.dataStore.load();
}
if (tableConfig.isFixed()) {
tableElement.setMaxHeight(tableConfig.getFixedBodyHeight());
}
if (tableConfig.isFixed()) {
dui_datatable_width_full.remove(this);
this.addCss(dui_datatable_fixed);
ColumnUtils.fixElementWidth(this, tableElement.element());
}
if (tableConfig.isFixed()) {
ColumnUtils.fixElementWidth(this, tableElement.element());
}
return this;
}
/**
* Redraws the data table by updating the headers and reloading the data.
*
* @return the current DataTable instance
*/
public DataTable redraw() {
tableConfig.onBeforeHeaders(this);
tableConfig.drawHeaders(this, thead);
tableConfig.onAfterHeaders(this);
load();
return this;
}
/**
* Loads the data into the data table.
*
* @return the current DataTable instance
*/
public DataTable load() {
this.dataStore.load();
return this;
}
/**
* Sets the provided data to the data table.
*
* @param data the list of data to be set
* @return the current DataTable instance
*/
public DataTable setData(List data) {
this.data = data;
tableRows.clear();
removeRecordsHandler.removeRows(this);
if (nonNull(data) && !data.isEmpty()) {
addRows(data, 0);
}
return this;
}
/**
* Appends the provided data to the existing data in the table.
*
* @param newData the new data to be appended
* @return the current DataTable instance
*/
public DataTable appendData(List newData) {
if (nonNull(this.data)) {
addRows(newData, this.data.size());
this.data.addAll(newData);
} else {
setData(newData);
}
return this;
}
/**
* Adds rows to the data table based on the provided data and starting index.
*
* @param data the list of data to be added as rows
* @param initialIndex the starting index for the new rows
*/
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));
}
/**
* Returns the current data present in the table.
*
* @return the collection of data in the table
*/
public Collection getData() {
return data;
}
/**
* Sets the condensed style for the data table.
*
* @param condensed a boolean indicating whether to enable the condensed style
* @return the current DataTable instance
*/
public DataTable setCondensed(boolean condensed) {
this.addCss(BooleanCssClass.of(dui_datatable_condensed, condensed));
return this;
}
/**
* Checks if the data table has a condensed style.
*
* @return a boolean indicating if the table is condensed
*/
public boolean isCondensed() {
return dui_datatable_condensed.isAppliedTo(this);
}
/**
* Enables or disables the hover effect for the data table rows.
*
* @param hover a boolean indicating whether to enable the hover effect
* @return the current DataTable instance
*/
public DataTable setHover(boolean hover) {
this.addCss(BooleanCssClass.of(dui_datatable_hover, hover));
return this;
}
/**
* Checks if the hover effect is enabled for the data table rows.
*
* @return a boolean indicating if the hover effect is enabled
*/
public boolean isHover() {
return dui_datatable_hover.isAppliedTo(this);
}
/**
* Sets the bordered style for the data table.
*
* @param bordered a boolean indicating whether to enable the bordered style
* @return the current DataTable instance
*/
public DataTable setBordered(boolean bordered) {
this.addCss(BooleanCssClass.of(dui_datatable_bordered, bordered));
return this;
}
/**
* Checks if the data table has a bordered style.
*
* @return a boolean indicating if the table is bordered
*/
public boolean isBordered() {
return dui_datatable_bordered.isAppliedTo(this);
}
/**
* Sets the striped style for the data table rows.
*
* @param striped a boolean indicating whether to enable the striped style
* @return the current DataTable instance
*/
public DataTable setStriped(boolean striped) {
this.addCss(BooleanCssClass.of(dui_datatable_striped, striped));
return this;
}
/**
* Checks if the striped style is enabled for the data table rows.
*
* @return a boolean indicating if the striped style is enabled
*/
public boolean isStriped() {
return dui_datatable_striped.isAppliedTo(this);
}
/**
* Initiates the edit mode for all rows in the data table.
*
* @return the current DataTable instance
*/
public DataTable edit() {
getRows().forEach(TableRow::edit);
return this;
}
/**
* Saves any edits made to all rows in the data table.
*
* @return the current DataTable instance
*/
public DataTable save() {
getRows().forEach(TableRow::save);
return this;
}
/**
* Cancels the edit mode and discards any unsaved changes for all rows in the data table.
*
* @return the current DataTable instance
*/
public DataTable cancelEditing() {
getRows().forEach(TableRow::cancelEditing);
return this;
}
/**
* Retrieves the main table element associated with the data table.
*
* @return the table element
*/
public TableElement tableElement() {
return tableElement;
}
/**
* Retrieves the body element of the table, which contains the data rows.
*
* @return the table body element
*/
public TBodyElement bodyElement() {
return tbody;
}
/**
* Retrieves the header element of the table, which contains the column headers.
*
* @return the table header element
*/
public THeadElement headerElement() {
return thead;
}
/**
* Retrieves the footer element of the table, which might contain summary rows or other additional
* information.
*
* @return the table footer element
*/
public TFootElement footerElement() {
return tfoot;
}
/**
* Retrieves the configuration object associated with the data table, which defines columns,
* plugins, and other table settings.
*
* @return the table configuration object
*/
public TableConfig getTableConfig() {
return tableConfig;
}
/**
* Applies a provided filter to each row in the data table. Rows that pass the filter remain
* visible, while rows that fail the filter are marked as filtered and potentially hidden or
* styled differently.
*
* @param rowFilter the filter to apply to each row in the table
* @return the current DataTable instance
*/
public DataTable 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();
}
});
return this;
}
/**
* Removes any previously applied filters, making all rows visible.
*
* @return the current DataTable instance
*/
public DataTable 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();
});
return this;
}
/**
* Retrieves the root element associated with the data table.
*
* @return the root element of the data table
*/
@Override
public HTMLDivElement element() {
return root.element();
}
/**
* Retrieves a list of all the table rows that are currently selected.
*
* @return a list of selected table rows
*/
@Override
public List> getSelectedItems() {
return tableRows.stream().filter(TableRow::isSelected).collect(Collectors.toList());
}
/**
* Retrieves a list of the records associated with the table rows that are currently selected.
*
* @return a list of records corresponding to selected table rows
*/
public List getSelectedRecords() {
return tableRows.stream()
.filter(TableRow::isSelected)
.map(TableRow::getRecord)
.collect(Collectors.toList());
}
/**
* Retrieves a list of all the table rows.
*
* @return a list of all table rows
*/
@Override
public List> getRows() {
return tableRows;
}
/**
* Retrieves a list of all the root table rows.
*
* @return a list of root table rows
*/
public List> getRootRows() {
return getRows().stream().filter(TableRow::isRoot).collect(Collectors.toList());
}
/**
* Retrieves a list of records associated with the root table rows.
*
* @return a list of records corresponding to root table rows
*/
public List getRecords() {
return getRows().stream()
.filter(TableRow::isRoot)
.map(TableRow::getRecord)
.collect(Collectors.toList());
}
/**
* Retrieves a list of records that have been modified.
*
* @return a list of modified records
*/
public List getDirtyRecords() {
return getRows().stream().map(TableRow::getDirtyRecord).collect(Collectors.toList());
}
/** Selects all rows in the table, without any conditions. */
@Override
public void selectAll() {
selectAll((table, tableRow) -> true);
}
/**
* Selects all rows in the table that meet the provided selection condition.
*
* @param selectionCondition the condition determining which rows should be selected
* @return the current DataTable instance
*/
public DataTable selectAll(SelectionCondition selectionCondition) {
if (tableConfig.isMultiSelect() && !tableRows.isEmpty()) {
for (TableRow tableRow : tableRows) {
if (selectionCondition.isAllowSelection(this, tableRow)) {
withPauseSelectionListenersToggle(
true,
field -> {
tableRow.select();
});
}
}
triggerSelectionListeners(null, getSelection());
fireTableEvent(SelectAllEvent.of(true, selectionCondition));
}
return this;
}
/** Deselects all rows in the table, without any conditions. */
@Override
public void deselectAll() {
deselectAll((table, tableRow) -> true);
}
/**
* Deselects all rows in the table that meet the provided selection condition.
*
* @param selectionCondition the condition determining which rows should be deselected
* @return the current DataTable instance
*/
public DataTable deselectAll(SelectionCondition selectionCondition) {
if (!tableRows.isEmpty()) {
for (TableRow tableRow : tableRows) {
if (tableRow.isSelected()) {
if (selectionCondition.isAllowSelection(this, tableRow)) {
withPauseSelectionListenersToggle(
true,
field -> {
tableRow.deselect();
});
}
}
}
triggerDeselectionListeners(null, new ArrayList<>());
fireTableEvent(SelectAllEvent.of(false, selectionCondition));
}
return this;
}
/**
* Determines if the table rows are selectable.
*
* @return true if selectable, false otherwise
*/
@Override
public boolean isSelectable() {
return this.selectable;
}
/**
* Pauses the execution of selection listeners.
*
* @return the current DataTable instance
*/
@Override
public DataTable pauseSelectionListeners() {
this.selectionListenersPaused = true;
return this;
}
/**
* Resumes the execution of selection listeners.
*
* @return the current DataTable instance
*/
@Override
public DataTable resumeSelectionListeners() {
this.selectionListenersPaused = false;
return this;
}
/**
* Toggles the pausing of selection listeners based on the provided value.
*
* @param toggle true to pause the listeners, false to resume
* @return the current DataTable instance
*/
@Override
public DataTable togglePauseSelectionListeners(boolean toggle) {
this.selectionListenersPaused = toggle;
return this;
}
/**
* Retrieves the set of selection listeners.
*
* @return a set of selection listeners
*/
@Override
public Set, ? super List>>>
getSelectionListeners() {
return this.selectionListeners;
}
/**
* Retrieves the set of deselection listeners.
*
* @return a set of deselection listeners
*/
@Override
public Set, ? super List>>>
getDeselectionListeners() {
return this.deselectionListeners;
}
/**
* Determines if the selection listeners are currently paused.
*
* @return true if the listeners are paused, false otherwise
*/
@Override
public boolean isSelectionListenersPaused() {
return this.selectionListenersPaused;
}
/**
* Triggers the registered selection listeners.
*
* @param source the source TableRow that caused the selection change
* @param selection the list of currently selected rows
* @return the current DataTable instance
*/
@Override
public DataTable triggerSelectionListeners(TableRow source, List> selection) {
if (!this.selectionListenersPaused) {
this.selectionListeners.forEach(
selectionListener ->
selectionListener.onSelectionChanged(Optional.ofNullable(source), selection));
}
return this;
}
/**
* Triggers the registered deselection listeners.
*
* @param source the source TableRow that caused the deselection
* @param selection the list of currently selected rows
* @return the current DataTable instance
*/
@Override
public DataTable triggerDeselectionListeners(TableRow source, List> selection) {
if (!this.selectionListenersPaused) {
this.deselectionListeners.forEach(
selectionListener ->
selectionListener.onSelectionChanged(Optional.ofNullable(source), selection));
}
return this;
}
/**
* Retrieves the currently selected table rows.
*
* @return a list of selected table rows
*/
@Override
public List> getSelection() {
return getSelectedItems();
}
/**
* Registers a table event listener for the specified event type.
*
* @param type the event type
* @param listener the listener to be added
* @return the current DataTable instance
*/
public DataTable addTableEventListener(String type, TableEventListener listener) {
if (!events.containsKey(type)) {
events.put(type, new ArrayList<>());
}
events.get(type).add(listener);
return this;
}
/**
* Removes a registered table event listener for the specified event type.
*
* @param type the event type
* @param listener the listener to be removed
* @return the current DataTable instance
*/
public DataTable removeTableListener(String type, TableEventListener listener) {
if (events.containsKey(type)) {
events.get(type).remove(listener);
}
return this;
}
/**
* Fires a specified table event.
*
* @param tableEvent the event to fire
* @return the current DataTable instance
*/
public DataTable 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 this;
}
/**
* Retrieves the current search context.
*
* @return the current search context
*/
public SearchContext getSearchContext() {
return searchContext;
}
/**
* Sets the handler for removing records.
*
* @param removeRecordsHandler the handler to set
* @return the current DataTable instance
*/
public DataTable setRemoveRecordsHandler(RemoveRowsHandler removeRecordsHandler) {
if (nonNull(removeRecordsHandler)) {
this.removeRecordsHandler = removeRecordsHandler;
}
return this;
}
/**
* Retrieves the dynamic style sheet associated with the DataTable.
*
* @return the dynamic style sheet
*/
public DynamicStyleSheet> getDynamicStyleSheet() {
return dynamicStyleSheet;
}
/**
* Appends a given node to the table. If a dynamic stylesheet is set, it will also append the
* style element of that stylesheet to the root element.
*
* @param node the node to be appended
* @return the current DataTable instance
*/
@Override
public DataTable appendChild(Node node) {
super.appendChild(node);
if (nonNull(this.dynamicStyleSheet)) {
this.root.appendChild(this.dynamicStyleSheet.getStyleElement());
}
return this;
}
/**
* Appends a given text as a text node to the table. If a dynamic stylesheet is set, it will also
* append the style element of that stylesheet to the root element.
*
* @param text the text to be appended as a text node
* @return the current DataTable instance
*/
@Override
public DataTable appendChild(String text) {
super.appendChild(text);
if (nonNull(this.dynamicStyleSheet)) {
this.root.appendChild(this.dynamicStyleSheet.getStyleElement());
}
return this;
}
/**
* Provides direct access to the table element to apply custom configurations.
*
* @param handler the handler that defines the operations to be performed on the table element
* @return the current DataTable instance
*/
public DataTable withTable(ChildHandler, TableElement> handler) {
handler.apply(this, tableElement);
return this;
}
/**
* Provides direct access to the table body element to apply custom configurations.
*
* @param handler the handler that defines the operations to be performed on the table body
* element
* @return the current DataTable instance
*/
public DataTable withTableBody(ChildHandler, TBodyElement> handler) {
handler.apply(this, tbody);
return this;
}
/**
* Provides direct access to the table footer element to apply custom configurations.
*
* @param handler the handler that defines the operations to be performed on the table footer
* element
* @return the current DataTable instance
*/
public DataTable withTableFooter(ChildHandler, TFootElement> handler) {
handler.apply(this, tfoot);
return this;
}
/**
* Provides direct access to the table head element to apply custom configurations.
*
* @param handler the handler that defines the operations to be performed on the table head
* element
* @return the current DataTable instance
*/
public DataTable withTableHead(ChildHandler, THeadElement> handler) {
handler.apply(this, thead);
return this;
}
/**
* Appends a given element to the table. If a dynamic stylesheet is set, it will also append the
* style element of that stylesheet to the root element.
*
* @param isElement the element to be appended
* @return the current DataTable instance
*/
@Override
public DataTable appendChild(IsElement> isElement) {
super.appendChild(isElement);
if (nonNull(this.dynamicStyleSheet)) {
this.root.appendChild(this.dynamicStyleSheet.getStyleElement());
}
return this;
}
/**
* Represents a listener for selection changes in the table.
*
* @param the type of data in the table
*/
@FunctionalInterface
public interface SelectionChangeListener {
/**
* Invoked when the selection in the table changes.
*
* @param selectedTableRows the currently selected table rows
* @param selectedRecords the records associated with the selected rows
*/
void onSelectionChanged(List> selectedTableRows, List selectedRecords);
}
/**
* Represents a filter for local rows in the table.
*
* @param the type of data in the table
*/
public interface LocalRowFilter {
/**
* Determines whether a table row should be filtered.
*
* @param tableRow the table row to evaluate
* @return true if the row should be included in the filtered set, false otherwise
*/
boolean filter(TableRow tableRow);
}
/**
* Represents a handler for removing rows from the table.
*
* @param the type of data in the table
*/
public interface RemoveRowsHandler {
/**
* Handles the removal of rows from the table.
*
* @param table the table from which rows should be removed
*/
void removeRows(DataTable table);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy