Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2000-2024 Vaadin Ltd.
*
* 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 com.vaadin.flow.component.combobox;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.combobox.dataview.ComboBoxDataView;
import com.vaadin.flow.component.combobox.dataview.ComboBoxLazyDataView;
import com.vaadin.flow.component.combobox.dataview.ComboBoxListDataView;
import com.vaadin.flow.data.provider.ArrayUpdater;
import com.vaadin.flow.data.provider.BackEndDataProvider;
import com.vaadin.flow.data.provider.CallbackDataProvider;
import com.vaadin.flow.data.provider.CompositeDataGenerator;
import com.vaadin.flow.data.provider.DataChangeEvent;
import com.vaadin.flow.data.provider.DataCommunicator;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.provider.DataProviderWrapper;
import com.vaadin.flow.data.provider.DataViewUtils;
import com.vaadin.flow.data.provider.HasDataView;
import com.vaadin.flow.data.provider.HasLazyDataView;
import com.vaadin.flow.data.provider.HasListDataView;
import com.vaadin.flow.data.provider.InMemoryDataProvider;
import com.vaadin.flow.data.provider.ItemCountChangeEvent;
import com.vaadin.flow.data.provider.ListDataProvider;
import com.vaadin.flow.data.provider.Query;
import com.vaadin.flow.dom.PropertyChangeEvent;
import com.vaadin.flow.function.SerializableComparator;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.function.SerializableFunction;
import com.vaadin.flow.function.SerializablePredicate;
import com.vaadin.flow.function.SerializableSupplier;
import com.vaadin.flow.internal.JsonUtils;
import com.vaadin.flow.shared.Registration;
import elemental.json.JsonValue;
/**
* Internal class that encapsulates the data communication logic with the web
* component
*
* @param
* Type of individual items that are selectable in the combo box
*/
class ComboBoxDataController
implements HasDataView>,
HasListDataView>,
HasLazyDataView> {
private enum UserProvidedFilter {
UNDECIDED, YES, NO
}
private final class UpdateQueue implements ArrayUpdater.Update {
private final transient List queue = new ArrayList<>();
private UpdateQueue(int size) {
enqueue("$connector.updateSize", size);
// Triggers a size update on the client side.
// This is exclusively needed for supporting immediate update of the
// dropdown scroller size when the
// LazyDataView::setItemCountEstimate() has been called, i.e. as
// soon as the user opens the dropdown. Otherwise, the scroller
// size update would be triggered only after a manual scrolling to
// the next page, which is a bad UX.
ComboBoxDataController.this.comboBox.getElement()
.setProperty("size", size);
}
@Override
public void set(int start, List items) {
enqueue("$connector.set", start,
items.stream().collect(JsonUtils.asArray()),
ComboBoxDataController.this.lastFilter);
}
@Override
public void clear(int start, int length) {
enqueue("$connector.clear", start, length);
}
@Override
public void commit(int updateId) {
enqueue("$connector.confirm", updateId,
ComboBoxDataController.this.lastFilter);
queue.forEach(Runnable::run);
queue.clear();
}
private void enqueue(String name, Serializable... arguments) {
queue.add(() -> ComboBoxDataController.this.comboBox.getElement()
.callJsFunction(name, arguments));
}
}
/**
* Lazy loading updater, used when calling setDataProvider()
*/
private final ArrayUpdater arrayUpdater = new ArrayUpdater() {
@Override
public Update startUpdate(int sizeChange) {
return new UpdateQueue(sizeChange);
}
@Override
public void initialize() {
// NO-OP
}
};
private final ComboBoxBase, TItem, ?> comboBox;
private final SerializableSupplier localeSupplier;
private ComboBoxDataCommunicator dataCommunicator;
private final CompositeDataGenerator dataGenerator = new CompositeDataGenerator<>();
private UserProvidedFilter userProvidedFilter = UserProvidedFilter.UNDECIDED;
private boolean shouldForceServerSideFiltering = false;
// Filter set by the client when requesting data. It's sent back to client
// together with the response so client may know for what filter data is
// provided.
private String lastFilter;
private SerializableConsumer filterSlot = filter -> {
// Just ignore when setDataProvider has not been called
};
private Registration lazyOpenRegistration;
private Registration clearFilterOnCloseRegistration;
private Registration dataProviderListener = null;
/**
* Creates a new data controller for that combo box
*
* @param comboBox
* the combo box that this controller manages
* @param localeSupplier
* supplier for the current locale of the combo box
*/
@SuppressWarnings({ "rawtypes", "unchecked" })
ComboBoxDataController(ComboBoxBase, TItem, ?> comboBox,
SerializableSupplier localeSupplier) {
this.comboBox = comboBox;
this.localeSupplier = localeSupplier;
// Update client side filtering when data provider size changes
ComponentUtil.addListener(comboBox, ItemCountChangeEvent.class,
(ComponentEventListener) (e -> updateClientSideFiltering()));
}
/**
* Accesses the data communicator managed by this controller
*/
ComboBoxDataCommunicator getDataCommunicator() {
return dataCommunicator;
}
/**
* Accesses the data provider managed by this controller
*/
DataProvider getDataProvider() {
if (dataCommunicator != null) {
return dataCommunicator.getDataProvider();
}
return null;
}
/**
* Accesses the data generator managed by this controller
*/
CompositeDataGenerator getDataGenerator() {
return dataGenerator;
}
/**
* Updates the page size in the data communicator and triggers a full
* refresh
*/
void setPageSize(int pageSize) {
if (dataCommunicator != null) {
dataCommunicator.setPageSize(pageSize);
}
reset();
updateClientSideFiltering();
}
/**
* Called to notify this controller that the component has been attached
*/
void onAttach() {
DataProvider dataProvider = getDataProvider();
if (dataProvider != null) {
setupDataProviderListener(dataProvider);
}
clearFilterOnCloseRegistration = comboBox.getElement()
.addPropertyChangeListener("opened", this::clearFilterOnClose);
reset();
}
/**
* Called to notify this controller that the component has been detached
*/
void onDetach() {
if (dataProviderListener != null) {
dataProviderListener.remove();
dataProviderListener = null;
}
if (clearFilterOnCloseRegistration != null) {
clearFilterOnCloseRegistration.remove();
clearFilterOnCloseRegistration = null;
}
}
/**
* Refresh item data of the web component when data has been updated, or
* after changes that might affect the presentation / rendering of items
*/
void reset() {
lastFilter = null;
if (dataCommunicator != null) {
dataCommunicator.setRequestedRange(0, 0);
dataCommunicator.reset();
}
comboBox.runBeforeClientResponse(ui -> ui.getPage().executeJs(
// If-statement is needed because on the first attach this
// JavaScript is called before initializing the connector.
"if($0.$connector) $0.$connector.reset();",
comboBox.getElement()));
}
/**
* Called to confirm that an update has been processed by the client-side
* connector
*
* @param id
* the update identifier
*/
void confirmUpdate(int id) {
dataCommunicator.confirmUpdate(id);
}
/**
* Called when the client-side connector requests data
*/
void setRequestedRange(int start, int length, String filter) {
// If the filter is null, which indicates that the combo box was closed
// before, then reset the data communicator to force sending an update
// to the client connector. This covers an edge-case when using an empty
// lazy data provider and refreshing it before opening the combo box
// again. In that case the data provider thinks that the client should
// already be up-to-date from the refresh, as in both cases, refresh and
// empty data provider, the effective requested size is zero, which
// results in it not sending an update. However, the client needs to
// receive an update in order to clear the loading state from opening
// the combo box.
if (lastFilter == null) {
dataCommunicator.reset();
}
dataCommunicator.setRequestedRange(start, length);
filterSlot.accept(filter);
}
/**
* Called by the client-side connector to reset the data communicator
*/
void resetDataCommunicator() {
dataCommunicator.reset();
}
// ****************************************************
// List data view implementation
// ****************************************************
@Override
public ComboBoxListDataView getListDataView() {
return new ComboBoxListDataView<>(getDataCommunicator(), comboBox,
this::onInMemoryFilterOrSortingChange);
}
public ComboBoxListDataView setItems(
ComboBox.ItemFilter itemFilter, Collection items) {
ListDataProvider listDataProvider = DataProvider
.ofCollection(items);
setDataProvider(itemFilter, listDataProvider);
return getListDataView();
}
@SafeVarargs
public final ComboBoxListDataView setItems(
ComboBox.ItemFilter itemFilter, TItem... items) {
return setItems(itemFilter, new ArrayList<>(Arrays.asList(items)));
}
public ComboBoxListDataView setItems(
ComboBox.ItemFilter itemFilter,
ListDataProvider listDataProvider) {
setDataProvider(itemFilter, listDataProvider);
return getListDataView();
}
@Override
public ComboBoxListDataView setItems(
ListDataProvider dataProvider) {
setDataProvider(dataProvider);
return getListDataView();
}
// ****************************************************
// Lazy data view implementation
// ****************************************************
@Override
public ComboBoxLazyDataView getLazyDataView() {
return new ComboBoxLazyDataView<>(getDataCommunicator(), comboBox);
}
public ComboBoxLazyDataView setItemsWithFilterConverter(
CallbackDataProvider.FetchCallback fetchCallback,
SerializableFunction filterConverter) {
Objects.requireNonNull(fetchCallback, "Fetch callback cannot be null");
ComboBoxLazyDataView lazyDataView = setItemsWithFilterConverter(
fetchCallback, query -> {
throw new IllegalStateException(
"Trying to use exact size with a lazy loading component"
+ " without either providing a count callback for the"
+ " component to fetch the count of the items or a data"
+ " provider that implements the size query. Provide the "
+ "callback for fetching item count with%n"
+ "comboBox.getLazyDataView().withDefinedSize(CallbackDataProvider.CountCallback);"
+ "%nor switch to undefined size with%n"
+ "comboBox.getLazyDataView().withUndefinedSize()");
}, filterConverter);
lazyDataView.setItemCountUnknown();
return lazyDataView;
}
public ComboBoxLazyDataView setItemsWithFilterConverter(
CallbackDataProvider.FetchCallback fetchCallback,
CallbackDataProvider.CountCallback countCallback,
SerializableFunction filterConverter) {
setDataProvider(DataProvider.fromFilteringCallbacks(fetchCallback,
countCallback), filterConverter);
return getLazyDataView();
}
@Override
public ComboBoxLazyDataView setItems(
BackEndDataProvider dataProvider) {
setDataProvider(dataProvider);
return getLazyDataView();
}
// ****************************************************
// Generic data view implementation
// ****************************************************
@Override
public ComboBoxDataView getGenericDataView() {
return new ComboBoxDataView<>(dataCommunicator, comboBox);
}
@Override
public ComboBoxDataView setItems(
DataProvider dataProvider) {
setDataProvider(dataProvider);
return getGenericDataView();
}
@Override
public ComboBoxDataView setItems(
InMemoryDataProvider dataProvider) {
throw new UnsupportedOperationException();
}
public ComboBoxDataView setItems(
InMemoryDataProvider inMemoryDataProvider,
SerializableFunction> filterConverter) {
Objects.requireNonNull(filterConverter,
"FilterConverter cannot be null");
// We don't use DataProvider.withConvertedFilter() here because its
// implementation does not apply the filter converter if Query has a
// null filter
DataProvider convertedDataProvider = new DataProviderWrapper<>(
inMemoryDataProvider) {
@Override
protected SerializablePredicate getFilter(
Query query) {
final Optional> componentInMemoryFilter = DataViewUtils
.getComponentFilter(comboBox);
return Optional.ofNullable(inMemoryDataProvider.getFilter())
.orElse(item -> true)
.and(item -> filterConverter
.apply(query.getFilter().orElse("")).test(item))
.and(componentInMemoryFilter.orElse(item -> true));
}
};
// As well as for ListDataProvider, filtering will be handled in the
// client-side if the size of the data set is less than the page size.
if (userProvidedFilter == UserProvidedFilter.UNDECIDED) {
userProvidedFilter = UserProvidedFilter.NO;
}
return setItems(convertedDataProvider);
}
// ****************************************************
// Data provider implementation
// ****************************************************
public void setDataProvider(DataProvider dataProvider) {
setDataProvider(dataProvider, SerializableFunction.identity());
}
public void setDataProvider(ListDataProvider listDataProvider) {
if (userProvidedFilter == UserProvidedFilter.UNDECIDED) {
userProvidedFilter = UserProvidedFilter.NO;
}
// Cannot use the case insensitive contains shorthand from
// ListDataProvider since it wouldn't react to locale changes
ComboBox.ItemFilter defaultItemFilter = (item,
filterText) -> comboBox.getItemLabelGenerator().apply(item)
.toLowerCase(localeSupplier.get())
.contains(filterText.toLowerCase(localeSupplier.get()));
setDataProvider(defaultItemFilter, listDataProvider);
}
public void setDataProvider(ComboBox.FetchItemsCallback fetchItems,
SerializableFunction sizeCallback) {
Objects.requireNonNull(fetchItems, "Fetch callback cannot be null");
Objects.requireNonNull(sizeCallback, "Size callback cannot be null");
userProvidedFilter = UserProvidedFilter.YES;
setDataProvider(new CallbackDataProvider<>(
query -> fetchItems.fetchItems(query.getFilter().orElse(""),
query.getOffset(), query.getLimit()),
query -> sizeCallback.apply(query.getFilter().orElse(""))));
}
public void setDataProvider(ComboBox.ItemFilter itemFilter,
ListDataProvider listDataProvider) {
Objects.requireNonNull(listDataProvider,
"List data provider cannot be null");
setDataProvider(listDataProvider, filterText -> {
Optional> componentInMemoryFilter = DataViewUtils
.getComponentFilter(comboBox);
SerializablePredicate componentInMemoryFilterOrAlwaysPass = componentInMemoryFilter
.orElse(ignore -> true);
return item -> itemFilter.test(item, filterText)
&& componentInMemoryFilterOrAlwaysPass.test(item);
});
}
public void setDataProvider(
DataProvider dataProvider,
SerializableFunction filterConverter) {
Objects.requireNonNull(dataProvider,
"The data provider can not be null");
Objects.requireNonNull(filterConverter,
"filterConverter cannot be null");
if (userProvidedFilter == UserProvidedFilter.UNDECIDED) {
userProvidedFilter = UserProvidedFilter.YES;
}
// Fetch from data provider is enabled eagerly if the data provider
// is of in-memory type and it's not empty (no need to fetch from
// empty data provider). Otherwise, the fetch will be postponed until
// dropdown open event
final boolean enableFetch = dataProvider.isInMemory()
&& !DataCommunicator.EmptyDataProvider.class
.isAssignableFrom(dataProvider.getClass());
if (dataCommunicator == null) {
// Create data communicator with postponed initialisation
dataCommunicator = new ComboBoxDataCommunicator<>(comboBox,
dataGenerator, arrayUpdater,
data -> comboBox.getElement()
.callJsFunction("$connector.updateData", data),
comboBox.getElement().getNode(), enableFetch) {
@Override
public void reset() {
super.reset();
if (comboBox instanceof MultiSelectComboBox) {
// The data is destroyed and rebuilt on data
// communicator reset. When component renderers are
// used, this means that the nodeIds for the items
// should also be updated. However, the "selectedItems"
// property is manually set in "refreshValue()".
// Therefore, the selected items can contain obsolete
// nodeIds. For this reason, this value refresh is
// necessary.
comboBox.refreshValue();
}
}
};
dataCommunicator.setPageSize(comboBox.getPageSize());
} else {
// Enable/disable items fetch from data provider depending on the
// data provider type
dataCommunicator.setFetchEnabled(enableFetch);
}
comboBox.getRenderManager().scheduleRender();
comboBox.setValue(null);
SerializableFunction convertOrNull = filterText -> {
if (filterText == null) {
return null;
}
return filterConverter.apply(filterText);
};
SerializableConsumer> providerFilterSlot = dataCommunicator
.setDataProvider(dataProvider,
convertOrNull.apply(comboBox.getFilter()), false);
filterSlot = filter -> {
if (!Objects.equals(filter, lastFilter)) {
DataCommunicator.Filter objectFilter = new DataCommunicator.Filter<>(
convertOrNull.apply(filter), filter.isEmpty());
providerFilterSlot.accept(objectFilter);
lastFilter = filter;
}
};
shouldForceServerSideFiltering = userProvidedFilter == UserProvidedFilter.YES;
setupDataProviderListener(dataProvider);
reset();
updateClientSideFiltering();
userProvidedFilter = UserProvidedFilter.UNDECIDED;
if (lazyOpenRegistration == null && !enableFetch) {
// Register an opened listener to enable fetch and size queries to
// data provider when the dropdown opens.
lazyOpenRegistration = comboBox.getElement()
.addPropertyChangeListener("opened",
this::executeRegistration);
}
}
private void updateClientSideFiltering() {
if (dataCommunicator != null) {
setClientSideFilter(
!this.shouldForceServerSideFiltering && dataCommunicator
.getItemCount() <= comboBox.getPageSize());
}
}
private void setClientSideFilter(boolean clientSideFilter) {
comboBox.getElement().setProperty("_clientSideFilter",
clientSideFilter);
}
private void clearFilterOnClose(PropertyChangeEvent event) {
if (Boolean.FALSE.equals(event.getValue())) {
if (lastFilter != null && !lastFilter.isEmpty()) {
clearClientSideFilterAndUpdateInMemoryFilter();
}
}
}
/**
* Enables {@link DataCommunicator} to fetch items from {@link DataProvider}
* when the open property changes for a lazy combobox. Clean registration on
* initialization.
*
* @param event
* property change event for "open"
*/
private void executeRegistration(PropertyChangeEvent event) {
if (Boolean.TRUE.equals(event.getValue())) {
removeLazyOpenRegistration();
dataCommunicator.setFetchEnabled(true);
if (!comboBox.isAutoOpen()) {
setRequestedRange(0, comboBox.getPageSize(),
comboBox.getFilter());
}
}
}
private void removeLazyOpenRegistration() {
if (lazyOpenRegistration != null) {
lazyOpenRegistration.remove();
lazyOpenRegistration = null;
}
}
private void onInMemoryFilterOrSortingChange(
SerializablePredicate filter,
SerializableComparator sortComparator) {
dataCommunicator.setInMemorySorting(sortComparator);
clearClientSideFilterAndUpdateInMemoryFilter();
}
private void clearClientSideFilterAndUpdateInMemoryFilter() {
lastFilter = null;
filterSlot.accept("");
reset();
}
private void setupDataProviderListener(
DataProvider dataProvider) {
if (dataProviderListener != null) {
dataProviderListener.remove();
}
dataProviderListener = dataProvider.addDataProviderListener(e -> {
if (e instanceof DataChangeEvent.DataRefreshEvent) {
dataCommunicator
.refresh(((DataChangeEvent.DataRefreshEvent) e)
.getItem());
} else {
reset();
}
});
}
}