summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorHenri Sara <hesara@vaadin.com>2016-08-25 12:57:37 +0300
committerVaadin Code Review <review@vaadin.com>2016-09-12 10:26:25 +0000
commit323d43711c572d7d676e044f1720fcc419a3c9d6 (patch)
tree467e42072ef24be00163ea5d2ead4f346fa95ccf /server
parent376a4d5b897327e957483bd43e7075f180a02e8f (diff)
downloadvaadin-framework-323d43711c572d7d676e044f1720fcc419a3c9d6.tar.gz
vaadin-framework-323d43711c572d7d676e044f1720fcc419a3c9d6.zip
Update ComboBox for new DataSource and communication mechanism
This simplifies the client side state machine. This change does not modify the CSS class name v-filterselect. Change-Id: I2f4a6e5252045cb7698d582be90693e00961b342
Diffstat (limited to 'server')
-rw-r--r--server/src/main/java/com/vaadin/ui/ComboBox.java690
1 files changed, 690 insertions, 0 deletions
diff --git a/server/src/main/java/com/vaadin/ui/ComboBox.java b/server/src/main/java/com/vaadin/ui/ComboBox.java
new file mode 100644
index 0000000000..c76ae4a728
--- /dev/null
+++ b/server/src/main/java/com/vaadin/ui/ComboBox.java
@@ -0,0 +1,690 @@
+/*
+ * Copyright 2000-2016 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.ui;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import com.vaadin.data.HasValue;
+import com.vaadin.event.FieldEvents;
+import com.vaadin.event.FieldEvents.BlurEvent;
+import com.vaadin.event.FieldEvents.BlurListener;
+import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcImpl;
+import com.vaadin.event.FieldEvents.FocusEvent;
+import com.vaadin.event.FieldEvents.FocusListener;
+import com.vaadin.event.selection.SingleSelectionChange;
+import com.vaadin.event.selection.SingleSelectionListener;
+import com.vaadin.server.KeyMapper;
+import com.vaadin.server.Resource;
+import com.vaadin.server.ResourceReference;
+import com.vaadin.server.data.DataCommunicator;
+import com.vaadin.server.data.DataKeyMapper;
+import com.vaadin.server.data.DataSource;
+import com.vaadin.shared.Registration;
+import com.vaadin.shared.data.DataCommunicatorConstants;
+import com.vaadin.shared.data.selection.SelectionModel;
+import com.vaadin.shared.ui.combobox.ComboBoxClientRpc;
+import com.vaadin.shared.ui.combobox.ComboBoxConstants;
+import com.vaadin.shared.ui.combobox.ComboBoxServerRpc;
+import com.vaadin.shared.ui.combobox.ComboBoxState;
+
+import elemental.json.JsonObject;
+
+/**
+ * A filtering dropdown single-select. Items are filtered based on user input.
+ * Supports the creation of new items when a handler is set by the user.
+ *
+ * @param <T>
+ * item (bean) type in ComboBox
+ * @author Vaadin Ltd
+ */
+@SuppressWarnings("serial")
+public class ComboBox<T> extends
+ AbstractListing<T, ComboBox<T>.ComboBoxSelectionModel> implements
+ HasValue<T>, FieldEvents.BlurNotifier, FieldEvents.FocusNotifier {
+
+ /**
+ * Custom single selection model for ComboBox.
+ */
+ protected class ComboBoxSelectionModel implements SelectionModel.Single<T> {
+ private Optional<T> selectedItem = Optional.empty();
+ private List<SingleSelectionListener<T>> listeners = new ArrayList<>(2);
+
+ @Override
+ public void deselect(T item) {
+ selectedItem.ifPresent(selected -> {
+ if (selected.equals(item)) {
+ setValue(Optional.empty(), false);
+ }
+ });
+ }
+
+ /**
+ * Sets the selection. This method is primarily for internal use.
+ *
+ * @param value
+ * optional new value
+ * @param userOriginated
+ * true if the value comes from user input, false otherwise
+ */
+ protected void setValue(Optional<T> value, boolean userOriginated) {
+ if (selectedItem.equals(value)) {
+ return;
+ }
+ selectedItem = value;
+ String key = value
+ .map(v -> getDataCommunicator().getKeyMapper().key(v))
+ .orElse(null);
+ String selectedCaption = value
+ .map(v -> getItemCaptionProvider().apply(v)).orElse(null);
+ getRpcProxy(ComboBoxClientRpc.class).setSelectedItem(key,
+ selectedCaption);
+
+ fireEvent(userOriginated);
+ }
+
+ /**
+ * Adds a selection listener to this select. The listener is called when
+ * the value of this select is changed either by the user or
+ * programmatically.
+ *
+ * @param listener
+ * the value change listener, not null
+ * @return a registration for the listener
+ */
+ public Registration addSelectionListener(
+ SingleSelectionListener<T> listener) {
+ Objects.requireNonNull(listener, "listener cannot be null");
+ listeners.add(listener);
+ return () -> listeners.remove(listener);
+ }
+
+ @Override
+ public void select(T item) {
+ Objects.requireNonNull(item);
+ setValue(Optional.of(item), false);
+ }
+
+ @Override
+ public Optional<T> getSelectedItem() {
+ return selectedItem;
+ }
+
+ /**
+ * Fires a selection change event to all listeners.
+ *
+ * @param userOriginated
+ * true to indicate that the change was caused directly by a
+ * user action, false for programmatically set values
+ */
+ protected void fireEvent(boolean userOriginated) {
+ for (SingleSelectionListener<T> listener : listeners) {
+ listener.accept(new SingleSelectionChange<T>(ComboBox.this,
+ getSelectedItem().orElse(null), userOriginated));
+ }
+ }
+ }
+
+ /**
+ * Handler that adds a new item based on user input when the new items
+ * allowed mode is active.
+ */
+ @FunctionalInterface
+ public interface NewItemHandler extends Consumer<String>, Serializable {
+ }
+
+ /**
+ * ItemCaptionProvider can be used to customize the string shown to the user
+ * for an item.
+ *
+ * @see ComboBox#setItemCaptionProvider(ItemCaptionProvider)
+ * @param <T>
+ * item type in the combo box
+ */
+ @FunctionalInterface
+ public interface ItemCaptionProvider<T>
+ extends Function<T, String>, Serializable {
+ }
+
+ /**
+ * ItemStyleProvider can be used to add custom styles to combo box items
+ * shown in the popup. The CSS class name that will be added to the item
+ * style names is <tt>v-filterselect-item-[style name]</tt>.
+ *
+ * @see ComboBox#setItemStyleProvider(ItemStyleProvider)
+ * @param <T>
+ * item type in the combo box
+ */
+ @FunctionalInterface
+ public interface ItemStyleProvider<T>
+ extends Function<T, String>, Serializable {
+ }
+
+ /**
+ * ItemIconProvider can be used to add custom icons to combo box items shown
+ * in the popup.
+ *
+ * @see ComboBox#setItemIconProvider(ItemIconProvider)
+ * @param <T>
+ * item type in the combo box
+ */
+ @FunctionalInterface
+ public interface ItemIconProvider<T>
+ extends Function<T, Resource>, Serializable {
+ }
+
+ /**
+ * Filter can be used to customize the filtering of items based on user
+ * input.
+ *
+ * @see ComboBox#setFilter(ItemFilter)
+ * @param <T>
+ * item type in the combo box
+ */
+ @FunctionalInterface
+ public interface ItemFilter<T>
+ extends BiFunction<String, T, Boolean>, Serializable {
+ }
+
+ private ComboBoxServerRpc rpc = new ComboBoxServerRpc() {
+ @Override
+ public void createNewItem(String itemValue) {
+ // New option entered
+ if (getNewItemHandler() != null && itemValue != null
+ && itemValue.length() > 0) {
+ getNewItemHandler().accept(itemValue);
+ // rebuild list
+ filterstring = null;
+ }
+ }
+
+ @Override
+ public void setSelectedItem(String key) {
+ // it seems both of these happen, and mean empty selection...
+ if (key == null || "".equals(key)) {
+ getSelectionModel().setValue(Optional.empty(), true);
+ } else {
+ T item = getDataCommunicator().getKeyMapper().get(key);
+ getSelectionModel().setValue(Optional.ofNullable(item), true);
+ }
+ }
+
+ @Override
+ public void setFilter(String filterText) {
+ filterstring = filterText;
+ if (filterText != null) {
+ getDataCommunicator().setInMemoryFilter(
+ item -> filter.apply(filterstring, item));
+ } else {
+ getDataCommunicator().setInMemoryFilter(null);
+ }
+ }
+ };
+
+ private FocusAndBlurServerRpcImpl focusBlurRpc = new FocusAndBlurServerRpcImpl(
+ this) {
+ @Override
+ protected void fireEvent(Component.Event event) {
+ ComboBox.this.fireEvent(event);
+ }
+ };
+
+ private String filterstring;
+
+ /**
+ * Handler for new items entered by the user.
+ */
+ private NewItemHandler newItemHandler;
+
+ private ItemCaptionProvider<T> itemCaptionProvider = String::valueOf;
+
+ private ItemStyleProvider<T> itemStyleProvider = item -> null;
+ private ItemIconProvider<T> itemIconProvider = item -> null;
+
+ private ItemFilter<T> filter = (filterText, item) -> {
+ if (filterText == null) {
+ return true;
+ } else {
+ return getItemCaptionProvider().apply(item).toLowerCase(getLocale())
+ .contains(filterText.toLowerCase(getLocale()));
+ }
+ };
+
+ /**
+ * Constructs an empty combo box without a caption. The content of the combo
+ * box can be set with {@link #setDataSource(DataSource)} or
+ * {@link #setItems(Collection)}
+ */
+ public ComboBox() {
+ super(new DataCommunicator<T>() {
+ @Override
+ protected DataKeyMapper<T> createKeyMapper() {
+ return new KeyMapper<T>() {
+ @Override
+ public void remove(T removeobj) {
+ // never remove keys from ComboBox to support selection
+ // of items that are not currently visible
+ }
+ };
+ }
+ });
+ setSelectionModel(new ComboBoxSelectionModel());
+
+ init();
+ }
+
+ /**
+ * Constructs an empty combo box, whose content can be set with
+ * {@link #setDataSource(DataSource)} or {@link #setItems(Collection)}.
+ *
+ * @param caption
+ * the caption to show in the containing layout, null for no
+ * caption
+ */
+ public ComboBox(String caption) {
+ this();
+ setCaption(caption);
+ }
+
+ /**
+ * Constructs a combo box with a static in-memory data source with the given
+ * options.
+ *
+ * @param caption
+ * the caption to show in the containing layout, null for no
+ * caption
+ * @param options
+ * collection of options, not null
+ */
+ public ComboBox(String caption, Collection<T> options) {
+ this(caption, DataSource.create(options));
+ }
+
+ /**
+ * Constructs a combo box with the given data source.
+ *
+ * @param caption
+ * the caption to show in the containing layout, null for no
+ * caption
+ * @param dataSource
+ * the data source to use, not null
+ */
+ public ComboBox(String caption, DataSource<T> dataSource) {
+ this(caption);
+ setDataSource(dataSource);
+ }
+
+ /**
+ * Initialize the ComboBox with default settings and register client to
+ * server RPC implementation.
+ */
+ private void init() {
+ registerRpc(rpc);
+ registerRpc(focusBlurRpc);
+
+ addDataGenerator((T data, JsonObject jsonObject) -> {
+ jsonObject.put(DataCommunicatorConstants.NAME,
+ getItemCaptionProvider().apply(data));
+ String style = itemStyleProvider.apply(data);
+ if (style != null) {
+ jsonObject.put(ComboBoxConstants.STYLE, style);
+ }
+ Resource icon = itemIconProvider.apply(data);
+ if (icon != null) {
+ String iconUrl = ResourceReference
+ .create(icon, ComboBox.this, null).getURL();
+ jsonObject.put(ComboBoxConstants.ICON, iconUrl);
+ }
+ });
+ }
+
+ /**
+ * Gets the current placeholder text shown when the combo box would be
+ * empty.
+ *
+ * @see #setPlaceholder(String)
+ * @return the current placeholder string, or null if not enabled
+ */
+ public String getPlaceholder() {
+ return getState(false).placeholder;
+ }
+
+ /**
+ * Sets the placeholder string - a textual prompt that is displayed when the
+ * select would otherwise be empty, to prompt the user for input.
+ *
+ * @param placeholder
+ * the desired placeholder, or null to disable
+ */
+ public void setPlaceholder(String placeholder) {
+ getState().placeholder = placeholder;
+ }
+
+ /**
+ * Sets whether it is possible to input text into the field or whether the
+ * field area of the component is just used to show what is selected. By
+ * disabling text input, the comboBox will work in the same way as a
+ * {@link NativeSelect}
+ *
+ * @see #isTextInputAllowed()
+ *
+ * @param textInputAllowed
+ * true to allow entering text, false to just show the current
+ * selection
+ */
+ public void setTextInputAllowed(boolean textInputAllowed) {
+ getState().textInputAllowed = textInputAllowed;
+ }
+
+ /**
+ * Returns true if the user can enter text into the field to either filter
+ * the selections or enter a new value if {@link #isNewItemsAllowed()}
+ * returns true. If text input is disabled, the comboBox will work in the
+ * same way as a {@link NativeSelect}
+ *
+ * @return true if text input is allowed
+ */
+ public boolean isTextInputAllowed() {
+ return getState(false).textInputAllowed;
+ }
+
+ @Override
+ public void addBlurListener(BlurListener listener) {
+ addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
+ BlurListener.blurMethod);
+ }
+
+ @Override
+ public void removeBlurListener(BlurListener listener) {
+ removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
+ }
+
+ @Override
+ public void addFocusListener(FocusListener listener) {
+ addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
+ FocusListener.focusMethod);
+ }
+
+ @Override
+ public void removeFocusListener(FocusListener listener) {
+ removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
+ }
+
+ /**
+ * Returns the page length of the suggestion popup.
+ *
+ * @return the pageLength
+ */
+ public int getPageLength() {
+ return getState(false).pageLength;
+ }
+
+ /**
+ * Returns the suggestion pop-up's width as a CSS string.
+ *
+ * @see #setPopupWidth
+ * @since 7.7
+ * @return explicitly set popup width as CSS size string or null if not set
+ */
+ public String getPopupWidth() {
+ return getState(false).suggestionPopupWidth;
+ }
+
+ /**
+ * Sets the page length for the suggestion popup. Setting the page length to
+ * 0 will disable suggestion popup paging (all items visible).
+ *
+ * @param pageLength
+ * the pageLength to set
+ */
+ public void setPageLength(int pageLength) {
+ getState().pageLength = pageLength;
+ }
+
+ /**
+ * Returns whether the user is allowed to select nothing in the combo box.
+ *
+ * @return true if empty selection is allowed, false otherwise
+ */
+ public boolean isEmptySelectionAllowed() {
+ return getState(false).emptySelectionAllowed;
+ }
+
+ /**
+ * Sets whether the user is allowed to select nothing in the combo box. When
+ * true, a special empty item is shown to the user.
+ *
+ * @param emptySelectionAllowed
+ * true to allow not selecting anything, false to require
+ * selection
+ */
+ public void setEmptySelectionAllowed(boolean emptySelectionAllowed) {
+ getState().emptySelectionAllowed = emptySelectionAllowed;
+ }
+
+ /**
+ * Sets the suggestion pop-up's width as a CSS string. By using relative
+ * units (e.g. "50%") it's possible to set the popup's width relative to the
+ * ComboBox itself.
+ *
+ * @see #getPopupWidth()
+ * @since 7.7
+ * @param width
+ * the width
+ */
+ public void setPopupWidth(String width) {
+ getState().suggestionPopupWidth = width;
+ }
+
+ /**
+ * Sets whether to scroll the selected item visible (directly open the page
+ * on which it is) when opening the combo box popup or not.
+ *
+ * This requires finding the index of the item, which can be expensive in
+ * many large lazy loading containers.
+ *
+ * @param scrollToSelectedItem
+ * true to find the page with the selected item when opening the
+ * selection popup
+ */
+ public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
+ getState().scrollToSelectedItem = scrollToSelectedItem;
+ }
+
+ /**
+ * Returns true if the select should find the page with the selected item
+ * when opening the popup.
+ *
+ * @see #setScrollToSelectedItem(boolean)
+ *
+ * @return true if the page with the selected item will be shown when
+ * opening the popup
+ */
+ public boolean isScrollToSelectedItem() {
+ return getState(false).scrollToSelectedItem;
+ }
+
+ /**
+ * Gets the item caption provider that is used to produce the strings shown
+ * in the combo box for each item.
+ *
+ * @return the item caption provider used, not null
+ */
+ public ItemCaptionProvider<T> getItemCaptionProvider() {
+ return itemCaptionProvider;
+ }
+
+ /**
+ * Sets the item caption provider that is used to produce the strings shown
+ * in the combo box for each item. By default,
+ * {@link String#valueOf(Object)} is used.
+ *
+ * @param itemCaptionProvider
+ * the item caption provider to use, not null
+ */
+ public void setItemCaptionProvider(
+ ItemCaptionProvider<T> itemCaptionProvider) {
+ Objects.requireNonNull(itemCaptionProvider,
+ "Item caption providers must not be null");
+ this.itemCaptionProvider = itemCaptionProvider;
+ getDataCommunicator().reset();
+ }
+
+ /**
+ * Sets the item style provider that is used to produce custom styles for
+ * showing items in the popup. The CSS class name that will be added to the
+ * item style names is <tt>v-filterselect-item-[style name]</tt>. Returning
+ * null from the provider results in no custom style name being set.
+ *
+ * @param itemStyleProvider
+ * the item style provider to set, not null
+ */
+ public void setItemStyleProvider(ItemStyleProvider<T> itemStyleProvider) {
+ Objects.requireNonNull(itemStyleProvider,
+ "Item style providers must not be null");
+ this.itemStyleProvider = itemStyleProvider;
+ getDataCommunicator().reset();
+ }
+
+ /**
+ * Gets the currently used item style provider that is used to generate CSS
+ * class names for items. The default item style provider returns null for
+ * all items, resulting in no custom item class names being set.
+ *
+ * @see #setItemStyleProvider(ItemStyleProvider)
+ *
+ * @return the currently used item style provider, not null
+ */
+ public ItemStyleProvider<T> getItemStyleProvider() {
+ return itemStyleProvider;
+ }
+
+ /**
+ * Sets the item icon provider that is used to produce custom icons for
+ * showing items in the popup. The provider can return null for items with
+ * no icon.
+ *
+ * @param itemIconProvider
+ * the item icon provider to set, not null
+ */
+ public void setItemIconProvider(ItemIconProvider<T> itemIconProvider) {
+ Objects.requireNonNull(itemIconProvider,
+ "Item icon providers must not be null");
+ this.itemIconProvider = itemIconProvider;
+ getDataCommunicator().reset();
+ }
+
+ /**
+ * Gets the currently used item icon provider. The default item icon
+ * provider returns null for all items, resulting in no icons being used.
+ *
+ * @see #setItemIconProvider(ItemIconProvider)
+ *
+ * @return the currently used item icon provider, not null
+ */
+ public ItemIconProvider<T> getItemIconProvider() {
+ return itemIconProvider;
+ }
+
+ /**
+ * Sets the handler that is called when user types a new item. The creation
+ * of new items is allowed when a new item handler has been set.
+ *
+ * @param newItemHandler
+ * handler called for new items, null to only permit the
+ * selection of existing items
+ */
+ public void setNewItemHandler(NewItemHandler newItemHandler) {
+ this.newItemHandler = newItemHandler;
+ getState().allowNewItems = (newItemHandler != null);
+ markAsDirty();
+ }
+
+ /**
+ * Returns the handler called when the user enters a new item (not present
+ * in the data source).
+ *
+ * @return new item handler or null if none specified
+ */
+ public NewItemHandler getNewItemHandler() {
+ return newItemHandler;
+ }
+
+ // HasValue methods delegated to the selection model
+
+ /**
+ * Returns the filter used to customize the list based on user input.
+ *
+ * @return the current filter, not null
+ */
+ public ItemFilter<T> getFilter() {
+ return filter;
+ }
+
+ /**
+ * Sets the filter used to customize the list based on user input. The
+ * default filter checks case-insensitively that the input string is
+ * contained in the item caption.
+ *
+ * @param filter
+ * the filter function to use, not null
+ */
+ public void setFilter(ItemFilter<T> filter) {
+ Objects.requireNonNull(filter, "Item filter must not be null");
+ this.filter = filter;
+ }
+
+ @Override
+ public void setValue(T value) {
+ getSelectionModel().setValue(Optional.ofNullable(value), false);
+
+ }
+
+ @Override
+ public T getValue() {
+ return getSelectionModel().getSelectedItem().orElse(null);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Registration addValueChangeListener(
+ HasValue.ValueChangeListener<? super T> listener) {
+ return getSelectionModel().addSelectionListener(event -> {
+ ((ValueChangeListener<T>) listener)
+ .accept(new ValueChange<T>(event.getConnector(),
+ event.getValue(), event.isUserOriginated()));
+ });
+ }
+
+ @Override
+ protected ComboBoxState getState() {
+ return (ComboBoxState) super.getState();
+ }
+
+ @Override
+ protected ComboBoxState getState(boolean markAsDirty) {
+ return (ComboBoxState) super.getState(markAsDirty);
+ }
+
+}