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

org.teamapps.ux.component.table.Table Maven / Gradle / Ivy

There is a newer version: 0.9.194
Show newest version
/*-
 * ========================LICENSE_START=================================
 * TeamApps
 * ---
 * Copyright (C) 2014 - 2023 TeamApps.org
 * ---
 * 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.
 * =========================LICENSE_END==================================
 */
package org.teamapps.ux.component.table;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.teamapps.common.format.Color;
import org.teamapps.data.extract.*;
import org.teamapps.data.value.SortDirection;
import org.teamapps.data.value.Sorting;
import org.teamapps.dto.*;
import org.teamapps.event.Event;
import org.teamapps.icons.Icon;
import org.teamapps.ux.cache.record.DuplicateEntriesException;
import org.teamapps.ux.cache.record.EqualsAndHashCode;
import org.teamapps.ux.cache.record.ItemRange;
import org.teamapps.ux.component.Component;
import org.teamapps.ux.component.field.AbstractField;
import org.teamapps.ux.component.field.FieldMessage;
import org.teamapps.ux.component.field.validator.FieldValidator;
import org.teamapps.ux.component.infiniteitemview.AbstractInfiniteListComponent;
import org.teamapps.ux.component.infiniteitemview.RecordsChangedEvent;
import org.teamapps.ux.component.infiniteitemview.RecordsRemovedEvent;

import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Table extends AbstractInfiniteListComponent> implements Component {

	private static final Logger LOGGER = LoggerFactory.getLogger(Table.class);

	public final Event> onCellEditingStarted = new Event<>();
	public final Event> onCellEditingStopped = new Event<>();
	public final Event> onCellValueChanged = new Event<>();
	/**
	 * Fired when any number of rows is selected by the user.
	 */
	public final Event> onRowsSelected = new Event<>();
	/**
	 * Fired only when a single row is selected by the user.
	 */
	public final Event onSingleRowSelected = new Event<>();
	/**
	 * Fired only when multiple rows are selected by the user.
	 */
	public final Event> onMultipleRowsSelected = new Event<>();

	public final Event> onCellClicked = new Event<>();
	public final Event onSortingChanged = new Event<>();
	public final Event onTableDataRequest = new Event<>();
	public final Event> onColumnOrderChange = new Event<>();
	public final Event> onColumnSizeChange = new Event<>();

	private PropertyProvider propertyProvider = new BeanPropertyExtractor<>();
	private PropertyInjector propertyInjector = new BeanPropertyInjector<>();

	private int clientRecordIdCounter = 0;

	private List selectedRecords = List.of();
	private TableCellCoordinates activeEditorCell;

	private CustomEqualsAndHashCodeMap> transientChangesByRecordAndPropertyName = new CustomEqualsAndHashCodeMap<>(customEqualsAndHashCode);
	private CustomEqualsAndHashCodeMap>> cellMessages = new CustomEqualsAndHashCodeMap<>(customEqualsAndHashCode);
	private CustomEqualsAndHashCodeMap> markedCells = new CustomEqualsAndHashCodeMap<>(customEqualsAndHashCode);

	private final List> columns = new ArrayList<>();

	private boolean displayAsList; // list has no cell borders, table has. selection policy: list = row selection, table = cell selection
	private boolean forceFitWidth; //if true, force the widths of all columns to fit into the available space of the list
	private int rowHeight = 28;
	private boolean stripedRows = true;
	private boolean hideHeaders; //if true, do not show any headers
	private boolean allowMultiRowSelection = false;
	private boolean showRowCheckBoxes; //if true, show check boxes on the left
	private boolean showNumbering; //if true, show numbering on the left
	private boolean textSelectionEnabled = true;

	private Sorting sorting; // nullable

	private boolean editable; //only valid for tables
	private boolean ensureEmptyLastRow; //if true, there is always an empty last row, as soon as any data is inserted into the empty row a new empty row is inserted

	private boolean treeMode; //if true, use the parent id property of UiDataRecord to display the table as tree
	private String indentedColumnName; // if set, indent this column depending on the depth in the data hierarchy
	private int indentation = 15; // in pixels

	private SelectionFrame selectionFrame;

	// ----- header -----

	private boolean showHeaderRow = false;
	private int headerRowHeight = 28;

	// ----- footer -----

	private boolean showFooterRow = false;
	private int footerRowHeight = 28;

	private final List topNonModelRecords = new ArrayList<>();
	private final List bottomNonModelRecords = new ArrayList<>();

	private Function contextMenuProvider = null;
	private int lastSeenContextMenuRequestId;
	private int rowBorderWidth;

	public Table() {
		this(new ArrayList<>());
	}

	public Table(List> columns) {
		super(new ListTableModel<>());
		columns.forEach(this::addColumn);
	}

	public static  Table create() {
		return new Table<>();
	}

	public void addColumn(TableColumn column) {
		addColumn(column, columns.size());
	}

	public void addColumn(TableColumn column, int index) {
		addColumns(List.of(column), index);
	}

	public  TableColumn addColumn(String propertyName, String title, AbstractField field) {
		return addColumn(propertyName, null, title, field, TableColumn.DEFAULT_WIDTH);
	}

	public  TableColumn addColumn(String propertyName, Icon icon, String title, AbstractField field) {
		return addColumn(propertyName, icon, title, field, TableColumn.DEFAULT_WIDTH);
	}

	public  TableColumn addColumn(String propertyName, Icon icon, String title, AbstractField field, int defaultWidth) {
		TableColumn column = new TableColumn<>(propertyName, icon, title, field, defaultWidth);
		addColumn(column);
		return column;
	}

	public void addColumns(List> newColumns, int index) {
		this.columns.addAll(index, newColumns);
		newColumns.forEach(column -> {
			column.setTable(this);
			AbstractField field = column.getField();
			field.setParent(this);
		});
		if (isRendered()) {
			getSessionContext().queueCommand(
					new UiTable.AddColumnsCommand(getId(), newColumns.stream()
							.map(TableColumn::createUiTableColumn)
							.collect(Collectors.toList()), index)
			);
			refreshData();
		}
	}

	public void removeColumn(String propertyName) {
		columns.stream()
				.filter(c -> Objects.equals(c.getPropertyName(), propertyName))
				.findFirst()
				.ifPresent(this::removeColumn);
	}

	public void removeColumn(TableColumn column) {
		this.removeColumns(Collections.singletonList(column));
	}

	public void removeColumns(List> obsoleteColumns) {
		this.columns.removeAll(obsoleteColumns);
		if (isRendered()) {
			getSessionContext().queueCommand(
					new UiTable.RemoveColumnsCommand(getId(), obsoleteColumns.stream()
							.map(TableColumn::getPropertyName)
							.collect(Collectors.toList()))
			);
		}
	}

	@Override
	protected void preRegisteringModel(TableModel model) {
		// this should be done before registering the model, so we don't have to handle the corresponding model events
		model.setSorting(sorting);
	}

	@Override
	public UiComponent createUiComponent() {
		List columns = this.columns.stream()
				.map(TableColumn::createUiTableColumn)
				.collect(Collectors.toList());
		UiTable uiTable = new UiTable(columns);
		mapAbstractUiComponentProperties(uiTable);
		uiTable.setSelectionFrame(selectionFrame != null ? selectionFrame.createUiSelectionFrame() : null);
		uiTable.setDisplayAsList(displayAsList);
		uiTable.setForceFitWidth(forceFitWidth);
		uiTable.setRowHeight(rowHeight);
		uiTable.setStripedRows(stripedRows);
		uiTable.setHideHeaders(hideHeaders);
		uiTable.setAllowMultiRowSelection(allowMultiRowSelection);
		uiTable.setShowRowCheckBoxes(showRowCheckBoxes);
		uiTable.setShowNumbering(showNumbering);
		uiTable.setTextSelectionEnabled(textSelectionEnabled);
		uiTable.setSortField(sorting != null ? sorting.getFieldName() : null);
		uiTable.setSortDirection(sorting != null ? sorting.getSortDirection().toUiSortDirection() : null);
		uiTable.setEditable(editable);
		uiTable.setTreeMode(treeMode);
		uiTable.setIndentedColumnName(indentedColumnName);
		uiTable.setIndentation(indentation);
		uiTable.setShowHeaderRow(showHeaderRow);
		uiTable.setHeaderRowHeight(headerRowHeight);
		uiTable.setShowFooterRow(showFooterRow);
		uiTable.setFooterRowHeight(footerRowHeight);
		uiTable.setContextMenuEnabled(contextMenuProvider != null);
		return uiTable;
	}

	@Override
	public void handleUiEvent(UiEvent event) {
		switch (event.getUiEventType()) {
			case UI_TABLE_ROWS_SELECTED: {
				UiTable.RowsSelectedEvent rowsSelectedEvent = (UiTable.RowsSelectedEvent) event;
				selectedRecords = renderedRecords.getRecords(rowsSelectedEvent.getRecordIds());
				this.onRowsSelected.fire(selectedRecords);
				if (selectedRecords.size() == 1) {
					this.onSingleRowSelected.fire(selectedRecords.get(0));
				} else if (selectedRecords.size() > 1) {
					this.onMultipleRowsSelected.fire(selectedRecords);
				}
				break;
			}
			case UI_TABLE_CELL_CLICKED: {
				UiTable.CellClickedEvent cellClickedEvent = (UiTable.CellClickedEvent) event;
				RECORD record = renderedRecords.getRecord(cellClickedEvent.getRecordId());
				TableColumn column = getColumnByPropertyName(cellClickedEvent.getColumnPropertyName());
				if (record != null && column != null) {
					this.onCellClicked.fire(new CellClickedEvent<>(record, column));
				}
				break;
			}
			case UI_TABLE_CELL_EDITING_STARTED: {
				UiTable.CellEditingStartedEvent editingStartedEvent = (UiTable.CellEditingStartedEvent) event;
				RECORD record = renderedRecords.getRecord(editingStartedEvent.getRecordId());
				TableColumn column = getColumnByPropertyName(editingStartedEvent.getColumnPropertyName());
				if (record == null || column == null) {
					return;
				}
				this.activeEditorCell = new TableCellCoordinates<>(record, editingStartedEvent.getColumnPropertyName());
				this.selectedRecords = List.of(activeEditorCell.getRecord());
				Object cellValue = getCellValue(record, column);
				AbstractField activeEditorField = getActiveEditorField();
				activeEditorField.setValue(cellValue);
				List cellMessages = getCellMessages(record, editingStartedEvent.getColumnPropertyName());
				List columnMessages = getColumnByPropertyName(editingStartedEvent.getColumnPropertyName()).getMessages();
				if (columnMessages == null) {
					columnMessages = Collections.emptyList();
				}
				List messages = new ArrayList<>(cellMessages);
				messages.addAll(columnMessages);
				activeEditorField.setCustomFieldMessages(messages);
				this.onCellEditingStarted.fire(new CellEditingStartedEvent(record, column, cellValue));
				break;
			}
			case UI_TABLE_CELL_EDITING_STOPPED: {
				this.activeEditorCell = null;
				UiTable.CellEditingStoppedEvent editingStoppedEvent = (UiTable.CellEditingStoppedEvent) event;
				RECORD record = renderedRecords.getRecord(editingStoppedEvent.getRecordId());
				TableColumn column = getColumnByPropertyName(editingStoppedEvent.getColumnPropertyName());
				if (record == null || column == null) {
					return;
				}
				this.onCellEditingStopped.fire(new CellEditingStoppedEvent<>(record, column));
				break;
			}
			case UI_TABLE_CELL_VALUE_CHANGED: {
				UiTable.CellValueChangedEvent changeEvent = (UiTable.CellValueChangedEvent) event;
				RECORD record = renderedRecords.getRecord(changeEvent.getRecordId());
				TableColumn column = this.getColumnByPropertyName(changeEvent.getColumnPropertyName());
				if (record == null || column == null) {
					return;
				}
				Object value = column.getField().convertUiValueToUxValue(changeEvent.getValue());
				transientChangesByRecordAndPropertyName
						.computeIfAbsent(record, idValue -> new HashMap<>())
						.put(column.getPropertyName(), value);
				onCellValueChanged.fire(new FieldValueChangedEventData(record, column, value));
				break;
			}
			case UI_TABLE_SORTING_CHANGED:
				UiTable.SortingChangedEvent sortingChangedEvent = (UiTable.SortingChangedEvent) event;
				var sortField = sortingChangedEvent.getSortField();
				var sortDirection = SortDirection.fromUiSortDirection(sortingChangedEvent.getSortDirection());
				this.sorting = sortField != null && sortDirection != null ? new Sorting(sortField, sortDirection) : null;
				getModel().setSorting(sorting);
				onSortingChanged.fire(new SortingChangedEventData(sortingChangedEvent.getSortField(), SortDirection.fromUiSortDirection(sortingChangedEvent.getSortDirection())));
				break;
			case UI_TABLE_DISPLAYED_RANGE_CHANGED: {
				UiTable.DisplayedRangeChangedEvent d = (UiTable.DisplayedRangeChangedEvent) event;
				try {
					handleScrollOrResize(ItemRange.startLength(d.getStartIndex(), d.getLength()));
				} catch (DuplicateEntriesException e) {
					// if the model returned a duplicate entry while scrolling, the underlying data apparently changed.
					// So try to refresh the whole data instead.
					LOGGER.warn("DuplicateEntriesException while retrieving data from model. This means the underlying data of the model has changed without the model notifying this component, so will refresh the whole data of this component.");
					refreshData();
				}
				break;
			}
			case UI_TABLE_FIELD_ORDER_CHANGE: {
				UiTable.FieldOrderChangeEvent fieldOrderChangeEvent = (UiTable.FieldOrderChangeEvent) event;
				TableColumn column = getColumnByPropertyName(fieldOrderChangeEvent.getColumnPropertyName());
				onColumnOrderChange.fire(new ColumnOrderChangeEventData<>(column, fieldOrderChangeEvent.getPosition()));
				break;
			}
			case UI_TABLE_COLUMN_SIZE_CHANGE: {
				UiTable.ColumnSizeChangeEvent columnSizeChangeEvent = (UiTable.ColumnSizeChangeEvent) event;
				TableColumn column = getColumnByPropertyName(columnSizeChangeEvent.getColumnPropertyName());
				onColumnSizeChange.fire(new ColumnSizeChangeEventData<>(column, columnSizeChangeEvent.getSize()));
				break;
			}
			case UI_TABLE_CONTEXT_MENU_REQUESTED: {
				UiTable.ContextMenuRequestedEvent e = (UiTable.ContextMenuRequestedEvent) event;
				lastSeenContextMenuRequestId = e.getRequestId();
				RECORD record = renderedRecords.getRecord(e.getRecordId());
				if (record != null && contextMenuProvider != null) {
					Component contextMenuContent = contextMenuProvider.apply(record);
					if (contextMenuContent != null) {
						queueCommandIfRendered(() -> new UiInfiniteItemView.SetContextMenuContentCommand(getId(), e.getRequestId(), contextMenuContent.createUiReference()));
					} else {
						queueCommandIfRendered(() -> new UiInfiniteItemView.CloseContextMenuCommand(getId(), e.getRequestId()));
					}
				} else {
					closeContextMenu();
				}
				break;
			}
		}
	}

	private  VALUE getCellValue(RECORD record, TableColumn column) {
		Map changesForRecord = transientChangesByRecordAndPropertyName.getOrDefault(record, Collections.emptyMap());
		boolean changed = changesForRecord.containsKey(column.getPropertyName());
		Object cellValue;
		if (changed) { // associated value might be null!!
			cellValue = changesForRecord.get(column.getPropertyName());
		} else {
			cellValue = extractRecordProperty(record, column);
		}
		return (VALUE) cellValue;
	}

	private  VALUE extractRecordProperty(RECORD record, TableColumn column) {
		if (column.getValueExtractor() != null) {
			return column.getValueExtractor().extract(record);
		} else {
			return (VALUE) propertyProvider.getValues(record, Collections.singletonList(column.getPropertyName())).get(column.getPropertyName());
		}
	}

	private Map extractRecordProperties(RECORD record) {
		Map>> columnsWithAndWithoutValueExtractor = columns.stream().collect(Collectors.partitioningBy(c -> c.getValueExtractor() != null));
		Map valuesByPropertyName = new HashMap<>(propertyProvider.getValues(record, columnsWithAndWithoutValueExtractor.get(false).stream()
				.map(TableColumn::getPropertyName)
				.collect(Collectors.toList())));
		columnsWithAndWithoutValueExtractor.get(true).forEach(recordTableColumn -> valuesByPropertyName.put(recordTableColumn.getPropertyName(),
				recordTableColumn.getValueExtractor().extract(record)));
		return valuesByPropertyName;
	}

	public List getColumnPropertyNames() {
		return columns.stream()
				.map(TableColumn::getPropertyName)
				.collect(Collectors.toList());
	}

	public TableCellCoordinates getActiveEditorCell() {
		return activeEditorCell;
	}

	public AbstractField getActiveEditorField() {
		if (activeEditorCell != null) {
			return getColumnByPropertyName(activeEditorCell.getPropertyName()).getField();
		} else {
			return null;
		}
	}

	public void setCellValue(RECORD record, String propertyName, Object value) {
		transientChangesByRecordAndPropertyName.computeIfAbsent(record, record1 -> new HashMap<>()).put(propertyName, value);
		UiIdentifiableClientRecord uiRecordIdOrNull = renderedRecords.getUiRecord(record);
		if (uiRecordIdOrNull != null) {
			final Object uiValue = getColumnByPropertyName(propertyName).getField().convertUxValueToUiValue(value);
			queueCommandIfRendered(() -> new UiTable.SetCellValueCommand(getId(), uiRecordIdOrNull.getId(), propertyName, uiValue));
		}
	}

	public void focusCell(RECORD record, String propertyName) {
		// TODO #field=component
	}

	public void setCellMarked(RECORD record, String propertyName, boolean mark) {
		if (mark) {
			markedCells.computeIfAbsent(record, record1 -> new HashSet<>()).add(propertyName);
		} else {
			Set markedCellPropertyNames = markedCells.getOrDefault(record, Collections.emptySet());
			if (markedCellPropertyNames.isEmpty()) {
				markedCells.remove(record);
			}
		}
		UiIdentifiableClientRecord uiRecordIdOrNull = renderedRecords.getUiRecord(record);
		if (uiRecordIdOrNull != null) {
			queueCommandIfRendered(() -> new UiTable.MarkTableFieldCommand(getId(), uiRecordIdOrNull.getId(), propertyName, mark));
		}
	}

	public void clearRecordMarkings(RECORD record) {
		markedCells.remove(record);
		updateSingleRecordOnClient(record);
	}

	public void clearAllCellMarkings() {
		markedCells.clear();
		queueCommandIfRendered(() -> new UiTable.ClearAllFieldMarkingsCommand(getId()));
	}

	// TODO implement using decider? more general formatting options?
	public void setRecordBold(RECORD record, boolean bold) {
		UiIdentifiableClientRecord uiRecordIdOrNull = renderedRecords.getUiRecord(record);
		if (uiRecordIdOrNull != null) {
			queueCommandIfRendered(() -> new UiTable.SetRecordBoldCommand(getId(), uiRecordIdOrNull.getId(), bold));
		}
	}

	public void setSelectedRecord(RECORD record) {
		setSelectedRecord(record, false);
	}

	public void setSelectedRecord(RECORD record, boolean scrollToRecordIfAvailable) {
		setSelectedRecords(record != null ? List.of(record) : List.of(), scrollToRecordIfAvailable);
	}

	public void setSelectedRecords(List records) {
		setSelectedRecords(records, false);
	}

	public void setSelectedRecords(List records, boolean scrollToFirstIfAvailable) {
		this.selectedRecords = records == null ? List.of() : List.copyOf(records);
		queueCommandIfRendered(() -> new UiTable.SelectRecordsCommand(getId(), renderedRecords.getUiRecordIds(selectedRecords), scrollToFirstIfAvailable));
	}

	public void setSelectedRow(int rowIndex) {
		setSelectedRow(rowIndex, false);
	}

	public void setSelectedRow(int rowIndex, boolean scrollTo) {
		getRecordByRowIndex(rowIndex).ifPresentOrElse(record -> {
			this.selectedRecords = List.of(record);
			queueCommandIfRendered(() -> new UiTable.SelectRowsCommand(getId(), List.of(rowIndex), scrollTo));
		}, () -> {
			this.selectedRecords = List.of();
			queueCommandIfRendered(() -> new UiTable.SelectRowsCommand(getId(), List.of(), scrollTo));
		});
	}

	public void setSelectedRows(List rowIndexes) {
		setSelectedRows(rowIndexes, false);
	}

	public void setSelectedRows(List rowIndexes, boolean scrollToFirst) {
		this.selectedRecords = getRecordsByRowIndexes(rowIndexes);
		queueCommandIfRendered(() -> new UiTable.SelectRowsCommand(getId(), rowIndexes, scrollToFirst));
	}

	private List getRecordsByRowIndexes(List rowIndexes) {
		return rowIndexes.stream()
				.flatMap((Integer rowIndex) -> getRecordByRowIndex(rowIndex).stream())
				.collect(Collectors.toList());
	}

	private Optional getRecordByRowIndex(int rowIndex) {
		List records = getModel().getRecords(rowIndex, 1);
		if (records.size() >= 1) {
			if (records.size() > 1) {
				LOGGER.warn("Got multiple records from model when only asking for one! Taking the first one.");
			}
			return Optional.of(records.get(0));
		} else {
			LOGGER.error("Could not find record at row index {}", rowIndex);
			return Optional.empty();
		}
	}

	protected void updateColumnMessages(TableColumn tableColumn) {
		queueCommandIfRendered(() -> new UiTable.SetColumnMessagesCommand(getId(), tableColumn.getPropertyName(), tableColumn.getMessages().stream()
				.map(message -> message.createUiFieldMessage(FieldMessage.Position.POPOVER, FieldMessage.Visibility.ON_HOVER_OR_FOCUS))
				.collect(Collectors.toList())));
	}

	public List getCellMessages(RECORD record, String propertyName) {
		return this.cellMessages.getOrDefault(record, Collections.emptyMap()).getOrDefault(propertyName, Collections.emptyList());
	}

	public void addCellMessage(RECORD record, String propertyName, FieldMessage message) {
		List cellMessages = this.cellMessages.computeIfAbsent(record, x -> new HashMap<>()).computeIfAbsent(propertyName, x -> new ArrayList<>());
		cellMessages.add(message);
		updateSingleCellMessages(record, propertyName, cellMessages);
	}

	public void removeCellMessage(RECORD record, String propertyName, FieldMessage message) {
		List cellMessages = this.cellMessages.computeIfAbsent(record, x -> new HashMap<>()).computeIfAbsent(propertyName, x -> new ArrayList<>());
		cellMessages.remove(message);
		updateSingleCellMessages(record, propertyName, cellMessages);
	}

	public List validateRecord(RECORD record) {
		return validateRowInternal(record, false);
	}

	public List validateRow(RECORD record) {
		return validateRowInternal(record, true);
	}

	private List validateRowInternal(RECORD record, boolean considerChangedCellValues) {
		Map stringObjectMap;
		stringObjectMap = extractRecordProperties(record);
		if (considerChangedCellValues) {
			stringObjectMap.putAll(getChangedCellValues(record));
		}
		//noinspection unchecked
		return getColumns().stream()
				.flatMap(column -> column.getField().getValidators().stream()
						.flatMap(validator -> {
							List messages = (((FieldValidator) validator).validate(stringObjectMap.get(column.getPropertyName())));
							if (messages != null) {
								return messages.stream();
							} else {
								return Stream.empty();
							}
						}))
				.collect(Collectors.toList());
	}

	private void updateSingleCellMessages(RECORD record, String propertyName, List cellMessages) {
		UiIdentifiableClientRecord uiRecordId = renderedRecords.getUiRecord(record);
		if (uiRecordId != null) {
			queueCommandIfRendered(() -> new UiTable.SetSingleCellMessagesCommand(
					getId(),
					uiRecordId.getId(),
					propertyName,
					cellMessages.stream()
							.map(m -> m.createUiFieldMessage(FieldMessage.Position.POPOVER, FieldMessage.Visibility.ON_HOVER_OR_FOCUS))
							.collect(Collectors.toList())
			));
		}
	}


	protected void updateColumnVisibility(TableColumn tableColumn) {
		queueCommandIfRendered(() -> new UiTable.SetColumnVisibilityCommand(getId(), tableColumn.getPropertyName(), tableColumn.isVisible()));
	}

	// TODO #focus propagation
//	@Override
//	public void handleFieldFocused(AbstractField field) {
//		if (selectedRecord != null) {
//			queueCommandIfRendered(() -> new UiTable.FocusCellCommand(getId(), clientRecordCache.getUiRecord(selectedRecord), field.getPropertyName()));
//		}
//	}

	public List getTopNonModelRecords() {
		return List.copyOf(topNonModelRecords);
	}

	public List getBottomNonModelRecords() {
		return List.copyOf(bottomNonModelRecords);
	}

	public List getNonModelRecords(boolean top) {
		return top ? getTopNonModelRecords() : getBottomNonModelRecords();
	}

	public void addTopNonModelRecord(RECORD record) {
		this.topNonModelRecords.add(0, record);
		refreshData();
	}

	public void addBottomNonModelRecord(RECORD record) {
		this.bottomNonModelRecords.add(record);
		refreshData();
	}

	public void addNonModelRecord(RECORD record, boolean addToTop) {
		if (addToTop) {
			addTopNonModelRecord(record);
		} else {
			addBottomNonModelRecord(record);
		}

	}

	public void removeTopNonModelRecord(RECORD record) {
		this.topNonModelRecords.remove(record);
		refreshData();
	}

	public void removeBottomNonModelRecord(RECORD record) {
		this.bottomNonModelRecords.remove(record);
		refreshData();
	}

	public void removeNonModelRecord(RECORD record) {
		this.topNonModelRecords.remove(record);
		this.bottomNonModelRecords.remove(record);
		refreshData();
	}

	public void removeNonModelRecord(RECORD record, boolean top) {
		if (top) {
			removeTopNonModelRecord(record);
		} else {
			removeBottomNonModelRecord(record);
		}
	}

	public void removeAllTopNonModelRecords() {
		this.topNonModelRecords.clear();
		refreshData();
	}

	public void removeAllBottomNonModelRecords() {
		this.topNonModelRecords.clear();
		refreshData();
	}

	public void removeAllNonModelRecords() {
		this.topNonModelRecords.clear();
		this.bottomNonModelRecords.clear();
		refreshData();
	}

	public void clearRecordMessages(RECORD record) {
		cellMessages.remove(record);
		updateSingleRecordOnClient(record);
	}

	public void updateRecordMessages(RECORD record, Map> messages) {
		cellMessages.put(record, new HashMap<>(messages));
		updateSingleRecordOnClient(record);
	}

	@Override
	protected void handleModelRecordsRemoved(RecordsRemovedEvent deleteEvent) {
		for (int i = Math.max(deleteEvent.getStart(), renderedRecords.getStartIndex()); i < Math.min(deleteEvent.getEnd(), renderedRecords.getEndIndex()); i++) {
			clearMetaDataForRecord(renderedRecords.getRecordByIndex(i));
		}
		super.handleModelRecordsRemoved(deleteEvent);
	}

	@Override
	protected void handleModelRecordsChanged(RecordsChangedEvent changeEvent) {
		for (int i = Math.max(changeEvent.getStart(), renderedRecords.getStartIndex()); i < Math.min(changeEvent.getEnd(), renderedRecords.getEndIndex()); i++) {
			clearMetaDataForRecord(renderedRecords.getRecordByIndex(i));
		}
		super.handleModelRecordsChanged(changeEvent);
	}

	private void clearMetaDataForRecord(RECORD record) {
		transientChangesByRecordAndPropertyName.remove(record);
		cellMessages.remove(record);
		markedCells.remove(record);
	}

	public void refreshData() {
		refresh();
	}

	@Override
	protected void sendUpdateDataCommandToClient(int start, List uiRecordIds, List newUiRecords, int totalNumberOfRecords) {
		queueCommandIfRendered(() -> {
			LOGGER.debug("SENDING: renderedRange.start: {}; uiRecordIds.size: {}; renderedRecords.size: {}; newUiRecords.size: {}; totalCount: {}",
					start, uiRecordIds.size(), renderedRecords.size(), newUiRecords.size(), totalNumberOfRecords);
			return new UiTable.UpdateDataCommand(
					getId(),
					start,
					uiRecordIds,
					(List) newUiRecords,
					totalNumberOfRecords
			);
		});
	}

	private int getTotalRecordsCount() {
		return getModelCount() + topNonModelRecords.size() + bottomNonModelRecords.size();
	}

	protected List retrieveRecords(int startIndex, int length) {
		if (startIndex < 0 || length < 0) {
			LOGGER.warn("Data coordinates do not make sense: startIndex {}, length {}", startIndex, length);
			return Collections.emptyList();
		}

		int endIndex = startIndex + length;
		int totalTopRecords = topNonModelRecords.size();
		int totalModelRecords = getModelCount();
		int totalBottomRecords = bottomNonModelRecords.size();

		if (endIndex > totalTopRecords + totalModelRecords + totalBottomRecords) {
			endIndex = Math.max(totalTopRecords + totalModelRecords + totalBottomRecords, startIndex);
			length = endIndex - startIndex;
		}

		if (startIndex < totalTopRecords && endIndex <= totalTopRecords) {
			return topNonModelRecords.stream().skip(startIndex).limit(length).collect(Collectors.toList());
		} else if (startIndex < totalTopRecords && endIndex <= totalTopRecords + totalModelRecords) {
			List records = new ArrayList<>();
			records.addAll(topNonModelRecords.stream().skip(startIndex).limit(totalTopRecords - startIndex).collect(Collectors.toList()));
			records.addAll(retrieveRecordsFromModel(0, length - records.size()));
			return records;
		} else if (startIndex < totalTopRecords && endIndex > totalTopRecords + totalModelRecords) {
			List records = new ArrayList<>();
			records.addAll(topNonModelRecords.stream().skip(startIndex).limit(totalTopRecords - startIndex).collect(Collectors.toList()));
			records.addAll(retrieveRecordsFromModel(0, totalModelRecords));
			records.addAll(bottomNonModelRecords.stream().skip(0).limit(length - records.size()).collect(Collectors.toList()));
			return records;
		} else if (startIndex >= totalTopRecords && startIndex < totalTopRecords + totalModelRecords && endIndex <= totalTopRecords + totalModelRecords) {
			return retrieveRecordsFromModel(startIndex - topNonModelRecords.size(), length);
		} else if (startIndex >= totalTopRecords && startIndex < totalTopRecords + totalModelRecords && endIndex > totalTopRecords + totalModelRecords) {
			List records = new ArrayList<>();
			records.addAll(retrieveRecordsFromModel(startIndex - topNonModelRecords.size(), endIndex - startIndex - totalTopRecords));
			records.addAll(bottomNonModelRecords.stream().skip(0).limit(length - records.size()).collect(Collectors.toList()));
			return records;
		} else if (startIndex >= totalTopRecords + totalModelRecords) {
			return bottomNonModelRecords.stream().skip(startIndex - totalTopRecords - totalModelRecords).limit(length).collect(Collectors.toList());
		} else {
			LOGGER.error("This path should never be reached!");
			return Collections.emptyList();
		}
	}

	private List retrieveRecordsFromModel(int startIndex, int length) {
		List records = getModel().getRecords(startIndex, length);
		if (records.size() == length) {
			return records;
		} else if (records.size() < length) {
			LOGGER.warn("TableModel did not return the requested amount of data!");
			return records;
		} else {
			LOGGER.warn("TableModel returned too much data. Truncating!");
			return new ArrayList<>(records.subList(0, length));
		}
	}

	public void cancelEditing() {
		TableCellCoordinates activeEditorCell = getActiveEditorCell();
		UiIdentifiableClientRecord uiRecord = renderedRecords.getUiRecord(activeEditorCell.getRecord());
		if (uiRecord != null) {
			queueCommandIfRendered(() -> new UiTable.CancelEditingCellCommand(getId(), uiRecord.getId(), activeEditorCell.getPropertyName()));
		}
	}

	private Map> createUiFieldMessagesForRecord(Map> recordFieldMessages) {
		return recordFieldMessages.entrySet().stream()
				.collect(Collectors.toMap(
						entry -> entry.getKey(),
						entry -> entry.getValue().stream()
								.map(fieldMessage -> fieldMessage.createUiFieldMessage(FieldMessage.Position.POPOVER, FieldMessage.Visibility.ON_HOVER_OR_FOCUS))
								.collect(Collectors.toList())
				));
	}

	@Override
	protected UiIdentifiableClientRecord createUiIdentifiableClientRecord(RECORD record) {
		UiTableClientRecord clientRecord = new UiTableClientRecord();
		clientRecord.setId(++clientRecordIdCounter);
		Map uxValues = extractRecordProperties(record);
		Map uiValues = columns.stream()
				.collect(HashMap::new, (map, column) -> map.put(column.getPropertyName(), ((AbstractField) column.getField()).convertUxValueToUiValue(uxValues.get(column.getPropertyName()))), HashMap::putAll);
		clientRecord.setValues(uiValues);
		clientRecord.setSelected(selectedRecords.stream().anyMatch(r -> customEqualsAndHashCode.getEquals().test(r, record)));
		clientRecord.setMessages(createUiFieldMessagesForRecord(cellMessages.getOrDefault(record, Collections.emptyMap())));
		clientRecord.setMarkings(new ArrayList<>(markedCells.getOrDefault(record, Collections.emptySet())));
		transientChangesByRecordAndPropertyName.getOrDefault(record, Map.of())
				.forEach((key, value) -> clientRecord.getValues().put(key, getColumnByPropertyName(key).getField().convertUxValueToUiValue(value)));
		return clientRecord;
	}

	public List> getColumns() {
		return columns;
	}

	public boolean isDisplayAsList() {
		return displayAsList;
	}

	public void setDisplayAsList(boolean displayAsList) {
		boolean changed = this.displayAsList != displayAsList;
		this.displayAsList = displayAsList;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public boolean isForceFitWidth() {
		return forceFitWidth;
	}

	public void setForceFitWidth(boolean forceFitWidth) {
		boolean changed = forceFitWidth != this.forceFitWidth;
		this.forceFitWidth = forceFitWidth;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	private UiRefreshableTableConfigUpdate createUiRefreshableTableConfigUpdate() {
		UiRefreshableTableConfigUpdate ui = new UiRefreshableTableConfigUpdate();
		ui.setForceFitWidth(forceFitWidth);
		ui.setRowHeight(rowHeight);
		ui.setAllowMultiRowSelection(allowMultiRowSelection);
		ui.setTextSelectionEnabled(textSelectionEnabled);
		ui.setEditable(editable);
		ui.setShowHeaderRow(showHeaderRow);
		ui.setHeaderRowHeight(headerRowHeight);
		ui.setShowFooterRow(showFooterRow);
		ui.setFooterRowHeight(footerRowHeight);
		return ui;
	}

	public int getRowHeight() {
		return rowHeight;
	}

	public void setRowHeight(int rowHeight) {
		boolean changed = rowHeight != this.rowHeight;
		this.rowHeight = rowHeight;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	public boolean isStripedRows() {
		return stripedRows;
	}

	public void setStripedRows(boolean stripedRows) {
		boolean changed = stripedRows != this.stripedRows;
		this.stripedRows = stripedRows;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setStripedRowColorEven(Color stripedRowColorEven) {
		this.setCssStyle(".striped-rows .slick-row.even", "background-color", stripedRowColorEven != null ? stripedRowColorEven.toHtmlColorString() : null);
	}

	public void setStripedRowColorOdd(Color stripedRowColorOdd) {
		this.setCssStyle(".striped-rows .slick-row.odd", "background-color", stripedRowColorOdd != null ? stripedRowColorOdd.toHtmlColorString() : null);
	}

	public boolean isHideHeaders() {
		return hideHeaders;
	}

	public void setHideHeaders(boolean hideHeaders) {
		boolean changed = hideHeaders != this.hideHeaders;
		this.hideHeaders = hideHeaders;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public boolean isAllowMultiRowSelection() {
		return allowMultiRowSelection;
	}

	public void setAllowMultiRowSelection(boolean allowMultiRowSelection) {
		boolean changed = allowMultiRowSelection != this.allowMultiRowSelection;
		this.allowMultiRowSelection = allowMultiRowSelection;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	public void setSelectionColor(Color selectionColor) {
		this.setCssStyle(".slick-cell.selected", "background-color", selectionColor != null ? selectionColor.toHtmlColorString() : null);
	}

	public void setRowBorderWidth(int rowBorderWidth) {
		boolean changed = rowBorderWidth != this.rowBorderWidth;
		this.rowBorderWidth = rowBorderWidth;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
		this.setCssStyle(".slick-cell", "border-bottom-width", rowBorderWidth + "px");
	}

	public void setRowBorderColor(Color rowBorderColor) {
		this.setCssStyle(".slick-cell", "border-color", rowBorderColor != null ? rowBorderColor.toHtmlColorString() : null);
	}

	public boolean isShowRowCheckBoxes() {
		return showRowCheckBoxes;
	}

	public void setShowRowCheckBoxes(boolean showRowCheckBoxes) {
		boolean changed = showRowCheckBoxes != this.showRowCheckBoxes;
		this.showRowCheckBoxes = showRowCheckBoxes;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public boolean isShowNumbering() {
		return showNumbering;
	}

	public void setShowNumbering(boolean showNumbering) {
		boolean changed = showNumbering != this.showNumbering;
		this.showNumbering = showNumbering;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public boolean isTextSelectionEnabled() {
		return textSelectionEnabled;
	}

	public void setTextSelectionEnabled(boolean textSelectionEnabled) {
		boolean changed = textSelectionEnabled != this.textSelectionEnabled;
		this.textSelectionEnabled = textSelectionEnabled;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	public Sorting getSorting() {
		return sorting;
	}

	public void setSorting(String sortField, SortDirection sortDirection) {
		setSorting(sortField != null && sortDirection != null ? new Sorting(sortField, sortDirection) : null);
	}

	public void setSorting(Sorting sorting) {
		this.sorting = sorting;
		TableModel model = getModel();
		if (model != null) {
			model.setSorting(sorting);
		}
	}

	public boolean isEditable() {
		return editable;
	}

	public void setEditable(boolean editable) {
		boolean changed = editable != this.editable;
		this.editable = editable;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	public boolean isEnsureEmptyLastRow() {
		return ensureEmptyLastRow;
	}

	public void setEnsureEmptyLastRow(boolean ensureEmptyLastRow) {
		boolean changed = ensureEmptyLastRow != this.ensureEmptyLastRow;
		this.ensureEmptyLastRow = ensureEmptyLastRow;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public boolean isTreeMode() {
		return treeMode;
	}

	public void setTreeMode(boolean treeMode) {
		boolean changed = treeMode != this.treeMode;
		this.treeMode = treeMode;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public String getIndentedColumnName() {
		return indentedColumnName;
	}

	public void setIndentedColumnName(String indentedColumnName) {
		boolean changed = !Objects.equals(indentedColumnName, this.indentedColumnName);
		this.indentedColumnName = indentedColumnName;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public int getIndentation() {
		return indentation;
	}

	public void setIndentation(int indentation) {
		boolean changed = indentation != this.indentation;
		this.indentation = indentation;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public SelectionFrame getSelectionFrame() {
		return selectionFrame;
	}

	public void setSelectionFrame(SelectionFrame selectionFrame) {
		boolean changed = !Objects.equals(selectionFrame, this.selectionFrame);
		this.selectionFrame = selectionFrame;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public boolean isShowHeaderRow() {
		return showHeaderRow;
	}

	public void setShowHeaderRow(boolean showHeaderRow) {
		boolean changed = showHeaderRow != this.showHeaderRow;
		this.showHeaderRow = showHeaderRow;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	public void setHeaderRowBorderWidth(int headerRowBorderWidth) {
		this.setCssStyle(".slick-headerrow", "border-bottom-width", headerRowBorderWidth + "px");
	}

	public void setHeaderRowBorderColor(Color headerRowBorderColor) {
		this.setCssStyle(".slick-headerrow", "border-bottom-color", headerRowBorderColor != null ? headerRowBorderColor.toHtmlColorString() : null);
	}

	public int getHeaderRowHeight() {
		return headerRowHeight;
	}

	public void setHeaderRowHeight(int headerRowHeight) {
		boolean changed = headerRowHeight != this.headerRowHeight;
		this.headerRowHeight = headerRowHeight;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	public void setHeaderRowBackgroundColor(Color headerRowBackgroundColor) {
		this.setCssStyle(".slick-headerrow", "background-color", headerRowBackgroundColor != null ? headerRowBackgroundColor.toHtmlColorString() : null);
	}

	/**
	 * Use {@link TableColumn#setHeaderRowField(AbstractField)} instead!
	 */
	@Deprecated
	public void setHeaderRowField(String columnName, AbstractField field) {
		getColumnByPropertyName(columnName).setHeaderRowField(field);
	}

	void updateHeaderRowField(TableColumn column) {
		queueCommandIfRendered(() -> new UiTable.SetHeaderRowFieldCommand(getId(), column.getPropertyName(), column.getHeaderRowField() != null ? column.getHeaderRowField().createUiReference() : null));
	}

	public boolean isShowFooterRow() {
		return showFooterRow;
	}

	public void setShowFooterRow(boolean showFooterRow) {
		boolean changed = showFooterRow != this.showFooterRow;
		this.showFooterRow = showFooterRow;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	public void setFooterRowBorderWidth(int footerRowBorderWidth) {
		this.setCssStyle(".slick-footerrow", "border-top-width", footerRowBorderWidth + "px");
	}

	public void setFooterRowBorderColor(Color footerRowBorderColor) {
		this.setCssStyle(".slick-footerrow", "border-top-color", footerRowBorderColor != null ? footerRowBorderColor.toHtmlColorString() : null);
	}

	public int getFooterRowHeight() {
		return footerRowHeight;
	}

	public void setFooterRowHeight(int footerRowHeight) {
		boolean changed = footerRowHeight != this.footerRowHeight;
		this.footerRowHeight = footerRowHeight;
		if (changed) {
			queueCommandIfRendered(() -> new UiTable.UpdateRefreshableConfigCommand(getId(), createUiRefreshableTableConfigUpdate()));
		}
	}

	public void setFooterRowBackgroundColor(Color footerRowBackgroundColor) {
		this.setCssStyle(".slick-footerrow", "background-color", footerRowBackgroundColor != null ? footerRowBackgroundColor.toHtmlColorString() : null);
	}

	/**
	 * Use {@link TableColumn#setFooterRowField(AbstractField)} instead!
	 */
	@Deprecated
	public void setFooterRowField(String columnName, AbstractField field) {
		getColumnByPropertyName(columnName).setFooterRowField(field);
	}

	void updateFooterRowField(TableColumn column) {
		queueCommandIfRendered(() -> new UiTable.SetFooterRowFieldCommand(getId(), column.getPropertyName(), column.getFooterRowField() != null ? column.getFooterRowField().createUiReference() : null));
	}

	public  TableColumn getColumnByPropertyName(String propertyName) {
		return columns.stream()
				.filter(column -> column.getPropertyName().equals(propertyName))
				.map(c -> (TableColumn) c)
				.findFirst().orElse(null);
	}

	public List getRecordsWithChangedCellValues() {
		return new ArrayList<>(transientChangesByRecordAndPropertyName.keySet());
	}

	public Map getChangedCellValues(RECORD record) {
		return transientChangesByRecordAndPropertyName.getOrDefault(record, Collections.emptyMap());
	}

	public Map getAllCellValuesForRecord(RECORD record) {
		Map values = extractRecordProperties(record);
		values.putAll(transientChangesByRecordAndPropertyName.getOrDefault(record, Collections.emptyMap()));
		return values;
	}

	public void clearChangeBuffer() {
		transientChangesByRecordAndPropertyName.clear();
	}

	public void applyCellValuesToRecord(RECORD record) {
		Map changedCellValues = getChangedCellValues(record);
		changedCellValues.forEach((propertyName, value) -> {
			ValueInjector columnValueInjector = getColumnByPropertyName(propertyName).getValueInjector();
			if (columnValueInjector != null) {
				columnValueInjector.inject(record, value);
			} else {
				propertyInjector.setValue(record, propertyName, value);
			}
		});
	}

	public void revertChanges() {
		transientChangesByRecordAndPropertyName.clear();
		refreshData();
	}

	public RECORD getSelectedRecord() {
		return selectedRecords.isEmpty() ? null : selectedRecords.get(selectedRecords.size() - 1);
	}

	public List getSelectedRecords() {
		return selectedRecords;
	}

	public PropertyProvider getPropertyProvider() {
		return propertyProvider;
	}

	public void setPropertyProvider(PropertyProvider propertyProvider) {
		this.propertyProvider = propertyProvider;
	}

	public void setPropertyExtractor(PropertyExtractor propertyExtractor) {
		this.setPropertyProvider(propertyExtractor);
	}

	public PropertyInjector getPropertyInjector() {
		return propertyInjector;
	}

	public void setPropertyInjector(PropertyInjector propertyInjector) {
		this.propertyInjector = propertyInjector;
	}

	public Function getContextMenuProvider() {
		return contextMenuProvider;
	}

	public void setContextMenuProvider(Function contextMenuProvider) {
		this.contextMenuProvider = contextMenuProvider;
	}

	public void closeContextMenu() {
		queueCommandIfRendered(() -> new UiInfiniteItemView.CloseContextMenuCommand(getId(), this.lastSeenContextMenuRequestId));
	}

	public List getRenderedRecords() {
		return renderedRecords.getRecords();
	}

	@Override
	public void setCustomEqualsAndHashCode(EqualsAndHashCode customEqualsAndHashCode) {
		super.setCustomEqualsAndHashCode(customEqualsAndHashCode);
		transientChangesByRecordAndPropertyName = new CustomEqualsAndHashCodeMap<>(customEqualsAndHashCode);
		cellMessages = new CustomEqualsAndHashCodeMap<>(customEqualsAndHashCode);
		markedCells = new CustomEqualsAndHashCodeMap<>(customEqualsAndHashCode);
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy