org.teamapps.ux.component.field.combobox.AbstractComboBox Maven / Gradle / Ivy
/*-
* ========================LICENSE_START=================================
* TeamApps
* ---
* Copyright (C) 2014 - 2024 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