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

org.teamapps.ux.component.field.combobox.AbstractComboBox 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.field.combobox;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.teamapps.data.extract.BeanPropertyExtractor;
import org.teamapps.data.extract.PropertyExtractor;
import org.teamapps.data.extract.PropertyProvider;
import org.teamapps.dto.*;
import org.teamapps.event.Event;
import org.teamapps.ux.cache.record.legacy.CacheManipulationHandle;
import org.teamapps.ux.cache.record.legacy.ClientRecordCache;
import org.teamapps.ux.component.field.AbstractField;
import org.teamapps.ux.component.field.SpecialKey;
import org.teamapps.ux.component.field.TextInputHandlingField;
import org.teamapps.ux.component.template.Template;
import org.teamapps.ux.component.tree.TreeNodeInfo;
import org.teamapps.ux.model.ComboBoxModel;

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

public abstract class AbstractComboBox extends AbstractField implements TextInputHandlingField {

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

	public final Event onTextInput = new Event<>();
	public final Event onSpecialKeyPressed = new Event<>();

	protected final ClientRecordCache recordCache;

	private ComboBoxModel model;
	private PropertyProvider propertyProvider = new BeanPropertyExtractor<>();

	private final Map templateIdsByTemplate = new HashMap<>();
	private int templateIdCounter = 0;
	private Template selectedEntryTemplate = null; // null: use recordToStringFunction;
	private Template dropDownTemplate = null; // null: use recordToStringFunction;
	private TemplateDecider selectedEntryTemplateDecider = record -> selectedEntryTemplate;
	private TemplateDecider dropdownTemplateDecider = record -> dropDownTemplate;

	private boolean dropDownButtonVisible = true;
	private boolean showDropDownAfterResultsArrive = false;
	private boolean highlightFirstResultEntry = true;
	private boolean autoComplete = true; // if true, by typing any letter, the first matching will be selected (keeping all not yet entered letters int the text box selected)
	private boolean showHighlighting = false; // TODO highlight any line of the template, but only corresponding to the textMatchingMode
	private int textHighlightingEntryLimit = 100;
	private boolean allowFreeText;
	private boolean showClearButton;
	private boolean animate = true;
	private boolean showExpanders = false;
	private String emptyText;
	private Integer dropDownMinWidth;
	private Integer dropDownMaxHeight;

	/**
	 * If true, already selected entries will be filtered out from model query results.
	 *
	 * @apiNote While this is handy when dealing with list-style models (no hierarchy), it might cause unintended behavior with tree-style
	 * models. This option will not pay any attention to hierarchical structures.
	 * If the parent P of a child node C is filtered out due to this option,
	 * then C will appear as a root node on the client side.
	 */
	private boolean distinctModelResultFiltering = false;

	private Function recordToStringFunction = Object::toString;
	protected Function freeTextRecordFactory = null;

	protected AbstractComboBox(ComboBoxModel model) {
		this.model = model;
		this.recordCache = new ClientRecordCache<>(this::createUiTreeRecordWithoutParentRelation, this::addParentLinkToUiRecord);
	}

	protected AbstractComboBox() {
		this(query -> Collections.emptyList());
	}

	protected void mapCommonUiComboBoxProperties(AbstractUiComboBox ui) {
		mapAbstractFieldAttributesToUiField(ui);

		// Note: it is important that the uiTemplates get set after the uiRecords are created, because custom templates (templateDecider) may lead to additional template registrations.
		ui.setTemplates(templateIdsByTemplate.entrySet().stream()
				.collect(Collectors.toMap(Map.Entry::getValue, entry -> entry.getKey().createUiTemplate())));

		ui.setShowDropDownButton(dropDownButtonVisible);
		ui.setShowDropDownAfterResultsArrive(showDropDownAfterResultsArrive);
		ui.setHighlightFirstResultEntry(highlightFirstResultEntry);
		ui.setShowHighlighting(showHighlighting);
		ui.setAutoComplete(autoComplete);
		ui.setTextHighlightingEntryLimit(textHighlightingEntryLimit);
		ui.setAllowAnyText(allowFreeText);
		ui.setShowClearButton(showClearButton);
		ui.setAnimate(animate);
		ui.setShowExpanders(showExpanders);
		ui.setPlaceholderText(emptyText);
		ui.setDropDownMinWidth(dropDownMinWidth);
		ui.setDropDownMaxHeight(dropDownMaxHeight);
	}

	@Override
	public void handleUiEvent(UiEvent event) {
		super.handleUiEvent(event);
		switch (event.getUiEventType()) {
			case UI_TEXT_INPUT_HANDLING_FIELD_TEXT_INPUT:
				UiTextInputHandlingField.TextInputEvent keyStrokeEvent = (UiTextInputHandlingField.TextInputEvent) event;
				String string = keyStrokeEvent.getEnteredString() != null ? keyStrokeEvent.getEnteredString() : ""; // prevent NPEs in combobox model implementations
				this.onTextInput.fire(string);
				break;
			case UI_TEXT_INPUT_HANDLING_FIELD_SPECIAL_KEY_PRESSED:
				UiTextInputHandlingField.SpecialKeyPressedEvent specialKeyPressedEvent = (UiTextInputHandlingField.SpecialKeyPressedEvent) event;
				this.onSpecialKeyPressed.fire(SpecialKey.valueOf(specialKeyPressedEvent.getKey().name()));
				break;
		}
	}

	@Override
	public Object handleUiQuery(UiQuery query) {
		switch (query.getUiQueryType()) {
			case ABSTRACT_UI_COMBO_BOX_RETRIEVE_DROPDOWN_ENTRIES: {
				final UiComboBox.RetrieveDropdownEntriesQuery q = (UiComboBox.RetrieveDropdownEntriesQuery) query;
				String string = q.getQueryString() != null ? q.getQueryString() : ""; // prevent NPEs in combobox model implementations
				if (model != null) {
					List resultRecords = model.getRecords(string);
					if (distinctModelResultFiltering) {
						resultRecords = filterOutSelected(resultRecords);
					}
					CacheManipulationHandle> cacheResponse = recordCache.replaceRecords(resultRecords);
					cacheResponse.commit();
					return cacheResponse.getAndClearResult();
				} else {
					return List.of();
				}
			}
			case ABSTRACT_UI_COMBO_BOX_LAZY_CHILDREN: {
				UiComboBox.LazyChildrenQuery lazyChildrenQuery = (UiComboBox.LazyChildrenQuery) query;
				RECORD parentRecord = recordCache.getRecordByClientId(lazyChildrenQuery.getParentId());
				if (parentRecord != null) {
					if (model != null) {
						List childRecords = model.getChildRecords(parentRecord);
						if (distinctModelResultFiltering) {
							childRecords = filterOutSelected(childRecords);
						}
						CacheManipulationHandle> cacheResponse = recordCache.addRecords(childRecords);
						cacheResponse.commit();
						return cacheResponse.getAndClearResult();
					}
				} else {
					return Collections.emptyList();
				}
			}
			default:
				return super.handleUiQuery(query);
		}
	}

	private List filterOutSelected(List resultRecords) {
		Set selectedRecords = getSelectedRecords();
		resultRecords = resultRecords.stream()
				.filter(r -> !selectedRecords.contains(r))
				.collect(Collectors.toList());
		return resultRecords;
	}

	protected abstract Set getSelectedRecords();

	protected UiComboBoxTreeRecord createUiTreeRecordWithoutParentRelation(RECORD record) {
		if (record == null) {
			return null;
		}
		// do not look for objects inside the cache here. they are sent to the client anyway. Also, values like expanded would have to be updated in any case.

		Template displayTemplate = getTemplateForRecord(record, selectedEntryTemplateDecider, selectedEntryTemplate);
		Template dropdownTemplate = getTemplateForRecord(record, dropdownTemplateDecider, dropDownTemplate);

		HashSet templatePropertyNames = new HashSet<>();
		templatePropertyNames.addAll(displayTemplate != null ? displayTemplate.getPropertyNames() : Collections.emptySet());
		templatePropertyNames.addAll(dropdownTemplate != null ? dropdownTemplate.getPropertyNames() : Collections.emptySet());
		Map values = propertyProvider.getValues(record, templatePropertyNames);
		UiComboBoxTreeRecord uiTreeRecord = new UiComboBoxTreeRecord();
		uiTreeRecord.setValues(values);

		uiTreeRecord.setDisplayTemplateId(templateIdsByTemplate.get(displayTemplate));
		uiTreeRecord.setDropDownTemplateId(templateIdsByTemplate.get(dropdownTemplate));
		uiTreeRecord.setAsString(this.recordToStringFunction.apply(record));

		TreeNodeInfo treeNodeInfo = model.getTreeNodeInfo(record);
		if (treeNodeInfo != null) {
			uiTreeRecord.setExpanded(treeNodeInfo.isExpanded());
			uiTreeRecord.setLazyChildren(treeNodeInfo.isLazyChildren());
			uiTreeRecord.setSelectable(treeNodeInfo.isSelectable());
		}

		return uiTreeRecord;
	}

	protected void addParentLinkToUiRecord(RECORD record, UiComboBoxTreeRecord uiTreeRecord, Map othersCurrentlyBeingAddedToCache) {
		TreeNodeInfo treeNodeInfo = model.getTreeNodeInfo(record);
		if (treeNodeInfo != null) {
			RECORD parent = (RECORD) treeNodeInfo.getParent();
			if (parent != null) {
				UiComboBoxTreeRecord uiParentFromOthers = othersCurrentlyBeingAddedToCache.get(parent);
				if (uiParentFromOthers != null) {
					uiTreeRecord.setParentId(uiParentFromOthers.getId());
				} else {
					Integer cachedParentUiRecordId = recordCache.getUiRecordIdOrNull(parent); // selectedRecordCache data is not hierarchical, so this is ok.
					if (cachedParentUiRecordId != null) {
						uiTreeRecord.setParentId(cachedParentUiRecordId);
					}
				}
			}
		}
	}

	private Template getTemplateForRecord(RECORD record, TemplateDecider templateDecider, Template defaultTemplate) {
		Template templateFromDecider = templateDecider.getTemplate(record);
		Template template = templateFromDecider != null ? templateFromDecider : defaultTemplate;
		if (template != null && !templateIdsByTemplate.containsKey(template)) {
			String uuid = "" + templateIdCounter++;
			this.templateIdsByTemplate.put(template, uuid);
			queueCommandIfRendered(() -> new UiComboBox.RegisterTemplateCommand(getId(), uuid, template.createUiTemplate()));
		}
		return template;
	}

	protected boolean isFreeTextEntry(UiComboBoxTreeRecord uiTreeRecord) {
		return uiTreeRecord.getId() < 0;
	}

	public boolean isAnimate() {
		return animate;
	}

	public ComboBoxModel getModel() {
		return model;
	}

	public void setModel(ComboBoxModel model) {
		this.model = model;
		reRenderIfRendered();
	}

	public boolean isDropDownButtonVisible() {
		return dropDownButtonVisible;
	}

	public boolean isShowDropDownAfterResultsArrive() {
		return showDropDownAfterResultsArrive;
	}

	public boolean isHighlightFirstResultEntry() {
		return highlightFirstResultEntry;
	}

	public boolean isAutoComplete() {
		return autoComplete;
	}

	public boolean isShowHighlighting() {
		return showHighlighting;
	}

	public int getTextHighlightingEntryLimit() {
		return textHighlightingEntryLimit;
	}

	public boolean isAllowFreeText() {
		return allowFreeText;
	}

	public boolean isShowClearButton() {
		return showClearButton;
	}

	public void setDropDownButtonVisible(boolean dropDownButtonVisible) {
		boolean changed = dropDownButtonVisible != this.dropDownButtonVisible;
		this.dropDownButtonVisible = dropDownButtonVisible;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setShowDropDownAfterResultsArrive(boolean showDropDownAfterResultsArrive) {
		boolean changed = showDropDownAfterResultsArrive != this.showDropDownAfterResultsArrive;
		this.showDropDownAfterResultsArrive = showDropDownAfterResultsArrive;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setHighlightFirstResultEntry(boolean highlightFirstResultEntry) {
		boolean changed = highlightFirstResultEntry != this.highlightFirstResultEntry;
		this.highlightFirstResultEntry = highlightFirstResultEntry;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setAutoComplete(boolean autoComplete) {
		boolean changed = autoComplete != this.autoComplete;
		this.autoComplete = autoComplete;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setShowHighlighting(boolean showHighlighting) {
		boolean changed = showHighlighting != this.showHighlighting;
		this.showHighlighting = showHighlighting;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setAllowFreeText(boolean allowFreeText) {
		boolean changed = allowFreeText != this.allowFreeText;
		this.allowFreeText = allowFreeText;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setShowClearButton(boolean showClearButton) {
		boolean changed = showClearButton != this.showClearButton;
		this.showClearButton = showClearButton;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setAnimate(boolean animate) {
		boolean changed = animate != this.animate;
		this.animate = animate;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public boolean isShowExpanders() {
		return showExpanders;
	}

	public void setShowExpanders(boolean showExpanders) {
		boolean changed = showExpanders != this.showExpanders;
		this.showExpanders = showExpanders;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public void setSelectedEntryTemplate(Template selectedEntryTemplate) {
		this.selectedEntryTemplate = selectedEntryTemplate;
		if (selectedEntryTemplate != null) {
			this.templateIdsByTemplate.put(selectedEntryTemplate, "" + templateIdCounter++);
		}
		reRenderIfRendered();
	}

	public void setDropDownTemplate(Template dropDownTemplate) {
		this.dropDownTemplate = dropDownTemplate;
		if (dropDownTemplate != null) {
			this.templateIdsByTemplate.put(dropDownTemplate, "" + templateIdCounter++);
		}
		reRenderIfRendered();
	}

	public void setTemplate(Template template) {
		setSelectedEntryTemplate(template);
		setDropDownTemplate(template);
	}

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

	public Template getSelectedEntryTemplate() {
		return selectedEntryTemplate;
	}

	public Template getDropDownTemplate() {
		return dropDownTemplate;
	}

	public TemplateDecider getSelectedEntryTemplateDecider() {
		return selectedEntryTemplateDecider;
	}

	public void setSelectedEntryTemplateDecider(TemplateDecider selectedEntryTemplateDecider) {
		this.selectedEntryTemplateDecider = selectedEntryTemplateDecider;
	}

	public TemplateDecider getDropdownTemplateDecider() {
		return dropdownTemplateDecider;
	}

	public void setDropdownTemplateDecider(TemplateDecider dropdownTemplateDecider) {
		this.dropdownTemplateDecider = dropdownTemplateDecider;
	}

	public void setTemplateDecider(TemplateDecider templateDecider) {
		this.selectedEntryTemplateDecider = templateDecider;
		this.dropdownTemplateDecider = templateDecider;
		reRenderIfRendered();
	}

	public Function getFreeTextRecordFactory() {
		return freeTextRecordFactory;
	}

	public void setFreeTextRecordFactory(Function freeTextRecordFactory) {
		this.freeTextRecordFactory = freeTextRecordFactory;
		reRenderIfRendered();
	}

	public Function getRecordToStringFunction() {
		return recordToStringFunction;
	}

	public void setRecordToStringFunction(Function recordToStringFunction) {
		this.recordToStringFunction = recordToStringFunction;
	}

	public PropertyProvider getPropertyProvider() {
		return propertyProvider;
	}

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

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

	public String getEmptyText() {
		return emptyText;
	}

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

	public boolean isDistinctModelResultFiltering() {
		return distinctModelResultFiltering;
	}

	public void setDistinctModelResultFiltering(boolean distinctModelResultFiltering) {
		this.distinctModelResultFiltering = distinctModelResultFiltering;
	}

	public Integer getDropDownMinWidth() {
		return dropDownMinWidth;
	}

	public void setDropDownMinWidth(Integer dropDownMinWidth) {
		boolean changed = !Objects.equals(dropDownMinWidth, this.dropDownMinWidth);
		this.dropDownMinWidth = dropDownMinWidth;
		if (changed) {
			reRenderIfRendered();
		}
	}

	public Integer getDropDownMaxHeight() {
		return dropDownMaxHeight;
	}

	public void setDropDownMaxHeight(Integer dropDownMaxHeight) {
		boolean changed = !Objects.equals(dropDownMaxHeight, this.dropDownMaxHeight);
		this.dropDownMaxHeight = dropDownMaxHeight;
		if (changed) {
			reRenderIfRendered();
		}
	}

	@Override
	public Event onTextInput() {
		return this.onTextInput;
	}

	@Override
	public Event onSpecialKeyPressed() {
		return this.onSpecialKeyPressed;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy