You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ComboBox.java 36KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041
  1. /*
  2. * Copyright 2000-2018 Vaadin Ltd.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. * use this file except in compliance with the License. You may obtain a copy of
  6. * the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. * License for the specific language governing permissions and limitations under
  14. * the License.
  15. */
  16. package com.vaadin.ui;
  17. import java.io.Serializable;
  18. import java.util.Arrays;
  19. import java.util.Collection;
  20. import java.util.HashMap;
  21. import java.util.Map;
  22. import java.util.Objects;
  23. import java.util.Optional;
  24. import java.util.Set;
  25. import java.util.logging.Level;
  26. import java.util.logging.Logger;
  27. import java.util.stream.Stream;
  28. import com.vaadin.data.provider.CallbackDataProvider;
  29. import com.vaadin.data.provider.DataChangeEvent;
  30. import com.vaadin.data.provider.DataCommunicator;
  31. import com.vaadin.data.provider.DataGenerator;
  32. import com.vaadin.data.provider.DataKeyMapper;
  33. import com.vaadin.data.provider.DataProvider;
  34. import com.vaadin.data.provider.InMemoryDataProvider;
  35. import com.vaadin.data.provider.ListDataProvider;
  36. import org.jsoup.nodes.Element;
  37. import com.vaadin.data.HasFilterableDataProvider;
  38. import com.vaadin.data.HasValue;
  39. import com.vaadin.data.ValueProvider;
  40. import com.vaadin.event.FieldEvents;
  41. import com.vaadin.event.FieldEvents.BlurEvent;
  42. import com.vaadin.event.FieldEvents.BlurListener;
  43. import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcDecorator;
  44. import com.vaadin.event.FieldEvents.FocusEvent;
  45. import com.vaadin.event.FieldEvents.FocusListener;
  46. import com.vaadin.server.ConnectorResource;
  47. import com.vaadin.server.KeyMapper;
  48. import com.vaadin.server.Resource;
  49. import com.vaadin.server.ResourceReference;
  50. import com.vaadin.server.SerializableBiPredicate;
  51. import com.vaadin.server.SerializableConsumer;
  52. import com.vaadin.server.SerializableFunction;
  53. import com.vaadin.server.SerializableToIntFunction;
  54. import com.vaadin.shared.Registration;
  55. import com.vaadin.shared.data.DataCommunicatorConstants;
  56. import com.vaadin.shared.ui.combobox.ComboBoxClientRpc;
  57. import com.vaadin.shared.ui.combobox.ComboBoxConstants;
  58. import com.vaadin.shared.ui.combobox.ComboBoxServerRpc;
  59. import com.vaadin.shared.ui.combobox.ComboBoxState;
  60. import com.vaadin.ui.declarative.DesignAttributeHandler;
  61. import com.vaadin.ui.declarative.DesignContext;
  62. import com.vaadin.ui.declarative.DesignFormatter;
  63. import elemental.json.JsonObject;
  64. /**
  65. * A filtering dropdown single-select. Items are filtered based on user input.
  66. * Supports the creation of new items when a handler is set by the user.
  67. *
  68. * @param <T>
  69. * item (bean) type in ComboBox
  70. * @author Vaadin Ltd
  71. */
  72. @SuppressWarnings("serial")
  73. public class ComboBox<T> extends AbstractSingleSelect<T>
  74. implements FieldEvents.BlurNotifier, FieldEvents.FocusNotifier,
  75. HasFilterableDataProvider<T, String> {
  76. /**
  77. * A callback method for fetching items. The callback is provided with a
  78. * non-null string filter, offset index and limit.
  79. *
  80. * @param <T>
  81. * item (bean) type in ComboBox
  82. * @since 8.0
  83. */
  84. @FunctionalInterface
  85. public interface FetchItemsCallback<T> extends Serializable {
  86. /**
  87. * Returns a stream of items that match the given filter, limiting the
  88. * results with given offset and limit.
  89. * <p>
  90. * This method is called after the size of the data set is asked from a
  91. * related size callback. The offset and limit are promised to be within
  92. * the size of the data set.
  93. *
  94. * @param filter
  95. * a non-null filter string
  96. * @param offset
  97. * the first index to fetch
  98. * @param limit
  99. * the fetched item count
  100. * @return stream of items
  101. */
  102. public Stream<T> fetchItems(String filter, int offset, int limit);
  103. }
  104. /**
  105. * Handler that adds a new item based on user input when the new items
  106. * allowed mode is active.
  107. * <p>
  108. * NOTE 1: If the new item is rejected the client must be notified of the
  109. * fact via ComboBoxClientRpc or selection handling won't complete.
  110. * </p>
  111. * <p>
  112. * NOTE 2: Selection handling must be completed separately if filtering the
  113. * data source with the same value won't include the new item in the initial
  114. * list of suggestions. Failing to do so will lead to selection handling
  115. * never completing and previous selection remaining on the server.
  116. * </p>
  117. *
  118. * @since 8.0
  119. * @deprecated Since 8.4 replaced by {@link NewItemProvider}.
  120. */
  121. @Deprecated
  122. @FunctionalInterface
  123. public interface NewItemHandler extends SerializableConsumer<String> {
  124. }
  125. /**
  126. * Provider function that adds a new item based on user input when the new
  127. * items allowed mode is active. After the new item handling is complete,
  128. * this function should return {@code Optional.of(text)} for the completion
  129. * of automatic selection handling. If automatic selection is not wished
  130. * for, always return {@code Optional.isEmpty()}.
  131. *
  132. * @since 8.4
  133. */
  134. @FunctionalInterface
  135. public interface NewItemProvider<T>
  136. extends SerializableFunction<String, Optional<T>> {
  137. }
  138. /**
  139. * Item style generator class for declarative support.
  140. * <p>
  141. * Provides a straightforward mapping between an item and its style.
  142. *
  143. * @param <T>
  144. * item type
  145. * @since 8.0
  146. */
  147. protected static class DeclarativeStyleGenerator<T>
  148. implements StyleGenerator<T> {
  149. private StyleGenerator<T> fallback;
  150. private Map<T, String> styles = new HashMap<>();
  151. public DeclarativeStyleGenerator(StyleGenerator<T> fallback) {
  152. this.fallback = fallback;
  153. }
  154. @Override
  155. public String apply(T item) {
  156. return styles.containsKey(item) ? styles.get(item)
  157. : fallback.apply(item);
  158. }
  159. /**
  160. * Sets a {@code style} for the {@code item}.
  161. *
  162. * @param item
  163. * a data item
  164. * @param style
  165. * a style for the {@code item}
  166. */
  167. protected void setStyle(T item, String style) {
  168. styles.put(item, style);
  169. }
  170. }
  171. private ComboBoxServerRpc rpc = new ComboBoxServerRpc() {
  172. @Override
  173. public void createNewItem(String itemValue) {
  174. // New option entered
  175. boolean added = false;
  176. if (itemValue != null && !itemValue.isEmpty()) {
  177. if (getNewItemProvider() != null) {
  178. Optional<T> item = getNewItemProvider().apply(itemValue);
  179. added = item.isPresent();
  180. // Fixes issue
  181. // https://github.com/vaadin/framework/issues/11343
  182. // Update the internal selection state immediately to avoid
  183. // client side hanging. This is needed for cases that user
  184. // interaction fires multi events (like adding and deleting)
  185. // on a new item during the same round trip.
  186. item.ifPresent(value -> {
  187. setSelectedItem(value, true);
  188. getDataCommunicator().reset();
  189. });
  190. } else if (getNewItemHandler() != null) {
  191. getNewItemHandler().accept(itemValue);
  192. // Up to the user to tell if no item was added.
  193. added = true;
  194. }
  195. }
  196. if (!added) {
  197. // New item was not handled.
  198. getRpcProxy(ComboBoxClientRpc.class).newItemNotAdded(itemValue);
  199. }
  200. }
  201. @Override
  202. public void setFilter(String filterText) {
  203. getState().currentFilterText = filterText;
  204. filterSlot.accept(filterText);
  205. }
  206. @Override
  207. public void resetForceDataSourceUpdate() {
  208. getState().forceDataSourceUpdate = false;
  209. }
  210. };
  211. /**
  212. * Handler for new items entered by the user.
  213. */
  214. @Deprecated
  215. private NewItemHandler newItemHandler;
  216. /**
  217. * Provider function for new items entered by the user.
  218. */
  219. private NewItemProvider<T> newItemProvider;
  220. private StyleGenerator<T> itemStyleGenerator = item -> null;
  221. private SerializableConsumer<String> filterSlot = filter -> {
  222. // Just ignore when neither setDataProvider nor setItems has been called
  223. };
  224. /**
  225. * Constructs an empty combo box without a caption. The content of the combo
  226. * box can be set with {@link #setDataProvider(DataProvider)} or
  227. * {@link #setItems(Collection)}
  228. */
  229. public ComboBox() {
  230. this(new DataCommunicator<T>() {
  231. @Override
  232. protected DataKeyMapper<T> createKeyMapper(
  233. ValueProvider<T, Object> identifierGetter) {
  234. return new KeyMapper<T>(identifierGetter) {
  235. @Override
  236. public void remove(T removeobj) {
  237. // never remove keys from ComboBox to support selection
  238. // of items that are not currently visible
  239. }
  240. };
  241. }
  242. });
  243. }
  244. /**
  245. * Constructs an empty combo box, whose content can be set with
  246. * {@link #setDataProvider(DataProvider)} or {@link #setItems(Collection)}.
  247. *
  248. * @param caption
  249. * the caption to show in the containing layout, null for no
  250. * caption
  251. */
  252. public ComboBox(String caption) {
  253. this();
  254. setCaption(caption);
  255. }
  256. /**
  257. * Constructs a combo box with a static in-memory data provider with the
  258. * given options.
  259. *
  260. * @param caption
  261. * the caption to show in the containing layout, null for no
  262. * caption
  263. * @param options
  264. * collection of options, not null
  265. */
  266. public ComboBox(String caption, Collection<T> options) {
  267. this(caption);
  268. setItems(options);
  269. }
  270. /**
  271. * Constructs and initializes an empty combo box.
  272. *
  273. * @param dataCommunicator
  274. * the data comnunicator to use with this ComboBox
  275. * @since 8.5
  276. */
  277. protected ComboBox(DataCommunicator<T> dataCommunicator) {
  278. super(dataCommunicator);
  279. init();
  280. }
  281. /**
  282. * Initialize the ComboBox with default settings and register client to
  283. * server RPC implementation.
  284. */
  285. private void init() {
  286. registerRpc(rpc);
  287. registerRpc(new FocusAndBlurServerRpcDecorator(this, this::fireEvent));
  288. addDataGenerator(new DataGenerator<T>() {
  289. /**
  290. * Map for storing names for icons.
  291. */
  292. private Map<Object, String> resourceKeyMap = new HashMap<>();
  293. private int counter = 0;
  294. @Override
  295. public void generateData(T item, JsonObject jsonObject) {
  296. String caption = getItemCaptionGenerator().apply(item);
  297. if (caption == null) {
  298. caption = "";
  299. }
  300. jsonObject.put(DataCommunicatorConstants.NAME, caption);
  301. String style = itemStyleGenerator.apply(item);
  302. if (style != null) {
  303. jsonObject.put(ComboBoxConstants.STYLE, style);
  304. }
  305. Resource icon = getItemIcon(item);
  306. if (icon != null) {
  307. String iconKey = resourceKeyMap
  308. .get(getDataProvider().getId(item));
  309. String iconUrl = ResourceReference
  310. .create(icon, ComboBox.this, iconKey).getURL();
  311. jsonObject.put(ComboBoxConstants.ICON, iconUrl);
  312. }
  313. }
  314. @Override
  315. public void destroyData(T item) {
  316. Object itemId = getDataProvider().getId(item);
  317. if (resourceKeyMap.containsKey(itemId)) {
  318. setResource(resourceKeyMap.get(itemId), null);
  319. resourceKeyMap.remove(itemId);
  320. }
  321. }
  322. private Resource getItemIcon(T item) {
  323. Resource icon = getItemIconGenerator().apply(item);
  324. if (icon == null || !(icon instanceof ConnectorResource)) {
  325. return icon;
  326. }
  327. Object itemId = getDataProvider().getId(item);
  328. if (!resourceKeyMap.containsKey(itemId)) {
  329. resourceKeyMap.put(itemId, "icon" + (counter++));
  330. }
  331. setResource(resourceKeyMap.get(itemId), icon);
  332. return icon;
  333. }
  334. });
  335. }
  336. /**
  337. * {@inheritDoc}
  338. * <p>
  339. * Filtering will use a case insensitive match to show all items where the
  340. * filter text is a substring of the caption displayed for that item.
  341. */
  342. @Override
  343. public void setItems(Collection<T> items) {
  344. ListDataProvider<T> listDataProvider = DataProvider.ofCollection(items);
  345. setDataProvider(listDataProvider);
  346. }
  347. /**
  348. * {@inheritDoc}
  349. * <p>
  350. * Filtering will use a case insensitive match to show all items where the
  351. * filter text is a substring of the caption displayed for that item.
  352. */
  353. @Override
  354. public void setItems(Stream<T> streamOfItems) {
  355. // Overridden only to add clarification to javadocs
  356. super.setItems(streamOfItems);
  357. }
  358. /**
  359. * {@inheritDoc}
  360. * <p>
  361. * Filtering will use a case insensitive match to show all items where the
  362. * filter text is a substring of the caption displayed for that item.
  363. */
  364. @Override
  365. public void setItems(T... items) {
  366. // Overridden only to add clarification to javadocs
  367. super.setItems(items);
  368. }
  369. /**
  370. * Sets a list data provider as the data provider of this combo box.
  371. * Filtering will use a case insensitive match to show all items where the
  372. * filter text is a substring of the caption displayed for that item.
  373. * <p>
  374. * Note that this is a shorthand that calls
  375. * {@link #setDataProvider(DataProvider)} with a wrapper of the provided
  376. * list data provider. This means that {@link #getDataProvider()} will
  377. * return the wrapper instead of the original list data provider.
  378. *
  379. * @param listDataProvider
  380. * the list data provider to use, not <code>null</code>
  381. * @since 8.0
  382. */
  383. public void setDataProvider(ListDataProvider<T> listDataProvider) {
  384. // Cannot use the case insensitive contains shorthand from
  385. // ListDataProvider since it wouldn't react to locale changes
  386. CaptionFilter defaultCaptionFilter = (itemText, filterText) -> itemText
  387. .toLowerCase(getLocale())
  388. .contains(filterText.toLowerCase(getLocale()));
  389. setDataProvider(defaultCaptionFilter, listDataProvider);
  390. }
  391. /**
  392. * Sets the data items of this listing and a simple string filter with which
  393. * the item string and the text the user has input are compared.
  394. * <p>
  395. * Note that unlike {@link #setItems(Collection)}, no automatic case
  396. * conversion is performed before the comparison.
  397. *
  398. * @param captionFilter
  399. * filter to check if an item is shown when user typed some text
  400. * into the ComboBox
  401. * @param items
  402. * the data items to display
  403. * @since 8.0
  404. */
  405. public void setItems(CaptionFilter captionFilter, Collection<T> items) {
  406. ListDataProvider<T> listDataProvider = DataProvider.ofCollection(items);
  407. setDataProvider(captionFilter, listDataProvider);
  408. }
  409. /**
  410. * Sets a list data provider with an item caption filter as the data
  411. * provider of this combo box. The caption filter is used to compare the
  412. * displayed caption of each item to the filter text entered by the user.
  413. *
  414. * @param captionFilter
  415. * filter to check if an item is shown when user typed some text
  416. * into the ComboBox
  417. * @param listDataProvider
  418. * the list data provider to use, not <code>null</code>
  419. * @since 8.0
  420. */
  421. public void setDataProvider(CaptionFilter captionFilter,
  422. ListDataProvider<T> listDataProvider) {
  423. Objects.requireNonNull(listDataProvider,
  424. "List data provider cannot be null");
  425. // Must do getItemCaptionGenerator() for each operation since it might
  426. // not be the same as when this method was invoked
  427. setDataProvider(listDataProvider, filterText -> item -> captionFilter
  428. .test(getItemCaptionOfItem(item), filterText));
  429. }
  430. // Helper method for the above to make lambda more readable
  431. private String getItemCaptionOfItem(T item) {
  432. String caption = getItemCaptionGenerator().apply(item);
  433. if (caption == null) {
  434. caption = "";
  435. }
  436. return caption;
  437. }
  438. /**
  439. * Sets the data items of this listing and a simple string filter with which
  440. * the item string and the text the user has input are compared.
  441. * <p>
  442. * Note that unlike {@link #setItems(Collection)}, no automatic case
  443. * conversion is performed before the comparison.
  444. *
  445. * @param captionFilter
  446. * filter to check if an item is shown when user typed some text
  447. * into the ComboBox
  448. * @param items
  449. * the data items to display
  450. * @since 8.0
  451. */
  452. public void setItems(CaptionFilter captionFilter,
  453. @SuppressWarnings("unchecked") T... items) {
  454. setItems(captionFilter, Arrays.asList(items));
  455. }
  456. /**
  457. * Gets the current placeholder text shown when the combo box would be
  458. * empty.
  459. *
  460. * @see #setPlaceholder(String)
  461. * @return the current placeholder string, or null if not enabled
  462. * @since 8.0
  463. */
  464. public String getPlaceholder() {
  465. return getState(false).placeholder;
  466. }
  467. /**
  468. * Sets the placeholder string - a textual prompt that is displayed when the
  469. * select would otherwise be empty, to prompt the user for input.
  470. *
  471. * @param placeholder
  472. * the desired placeholder, or null to disable
  473. * @since 8.0
  474. */
  475. public void setPlaceholder(String placeholder) {
  476. getState().placeholder = placeholder;
  477. }
  478. /**
  479. * Sets whether it is possible to input text into the field or whether the
  480. * field area of the component is just used to show what is selected. By
  481. * disabling text input, the comboBox will work in the same way as a
  482. * {@link NativeSelect}
  483. *
  484. * @see #isTextInputAllowed()
  485. *
  486. * @param textInputAllowed
  487. * true to allow entering text, false to just show the current
  488. * selection
  489. */
  490. public void setTextInputAllowed(boolean textInputAllowed) {
  491. getState().textInputAllowed = textInputAllowed;
  492. }
  493. /**
  494. * Returns true if the user can enter text into the field to either filter
  495. * the selections or enter a new value if new item provider or handler is
  496. * set (see {@link #setNewItemProvider(NewItemProvider)} (recommended) and
  497. * {@link #setNewItemHandler(NewItemHandler)} (deprecated)). If text input
  498. * is disabled, the comboBox will work in the same way as a
  499. * {@link NativeSelect}
  500. *
  501. * @return true if text input is allowed
  502. */
  503. public boolean isTextInputAllowed() {
  504. return getState(false).textInputAllowed;
  505. }
  506. @Override
  507. public Registration addBlurListener(BlurListener listener) {
  508. return addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
  509. BlurListener.blurMethod);
  510. }
  511. @Override
  512. public Registration addFocusListener(FocusListener listener) {
  513. return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
  514. FocusListener.focusMethod);
  515. }
  516. /**
  517. * Returns the page length of the suggestion popup.
  518. *
  519. * @return the pageLength
  520. */
  521. public int getPageLength() {
  522. return getState(false).pageLength;
  523. }
  524. /**
  525. * Returns the suggestion pop-up's width as a CSS string. By default this
  526. * width is set to "100%".
  527. *
  528. * @see #setPopupWidth
  529. * @since 7.7
  530. * @return explicitly set popup width as CSS size string or null if not set
  531. */
  532. public String getPopupWidth() {
  533. return getState(false).suggestionPopupWidth;
  534. }
  535. /**
  536. * Sets the page length for the suggestion popup. Setting the page length to
  537. * 0 will disable suggestion popup paging (all items visible).
  538. *
  539. * @param pageLength
  540. * the pageLength to set
  541. */
  542. public void setPageLength(int pageLength) {
  543. getState().pageLength = pageLength;
  544. }
  545. /**
  546. * Returns whether the user is allowed to select nothing in the combo box.
  547. *
  548. * @return true if empty selection is allowed, false otherwise
  549. * @since 8.0
  550. */
  551. public boolean isEmptySelectionAllowed() {
  552. return getState(false).emptySelectionAllowed;
  553. }
  554. /**
  555. * Sets whether the user is allowed to select nothing in the combo box. When
  556. * true, a special empty item is shown to the user.
  557. *
  558. * @param emptySelectionAllowed
  559. * true to allow not selecting anything, false to require
  560. * selection
  561. * @since 8.0
  562. */
  563. public void setEmptySelectionAllowed(boolean emptySelectionAllowed) {
  564. getState().emptySelectionAllowed = emptySelectionAllowed;
  565. }
  566. /**
  567. * Returns the empty selection caption.
  568. * <p>
  569. * The empty string {@code ""} is the default empty selection caption.
  570. *
  571. * @return the empty selection caption, not {@code null}
  572. * @see #setEmptySelectionAllowed(boolean)
  573. * @see #isEmptySelectionAllowed()
  574. * @see #setEmptySelectionCaption(String)
  575. * @see #isSelected(Object)
  576. * @since 8.0
  577. */
  578. public String getEmptySelectionCaption() {
  579. return getState(false).emptySelectionCaption;
  580. }
  581. /**
  582. * Sets the empty selection caption.
  583. * <p>
  584. * The empty string {@code ""} is the default empty selection caption.
  585. * <p>
  586. * If empty selection is allowed via the
  587. * {@link #setEmptySelectionAllowed(boolean)} method (it is by default) then
  588. * the empty item will be shown with the given caption.
  589. *
  590. * @param caption
  591. * the caption to set, not {@code null}
  592. * @see #isSelected(Object)
  593. * @since 8.0
  594. */
  595. public void setEmptySelectionCaption(String caption) {
  596. Objects.nonNull(caption);
  597. getState().emptySelectionCaption = caption;
  598. }
  599. /**
  600. * Sets the suggestion pop-up's width as a CSS string. By using relative
  601. * units (e.g. "50%") it's possible to set the popup's width relative to the
  602. * ComboBox itself.
  603. * <p>
  604. * By default this width is set to "100%" so that the pop-up's width is
  605. * equal to the width of the combobox. By setting width to null the pop-up's
  606. * width will automatically expand beyond 100% relative width to fit the
  607. * content of all displayed items.
  608. *
  609. * @see #getPopupWidth()
  610. * @since 7.7
  611. * @param width
  612. * the width
  613. */
  614. public void setPopupWidth(String width) {
  615. getState().suggestionPopupWidth = width;
  616. }
  617. /**
  618. * Sets whether to scroll the selected item visible (directly open the page
  619. * on which it is) when opening the combo box popup or not.
  620. * <p>
  621. * This requires finding the index of the item, which can be expensive in
  622. * many large lazy loading containers.
  623. *
  624. * @param scrollToSelectedItem
  625. * true to find the page with the selected item when opening the
  626. * selection popup
  627. */
  628. public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
  629. getState().scrollToSelectedItem = scrollToSelectedItem;
  630. }
  631. /**
  632. * Returns true if the select should find the page with the selected item
  633. * when opening the popup.
  634. *
  635. * @see #setScrollToSelectedItem(boolean)
  636. *
  637. * @return true if the page with the selected item will be shown when
  638. * opening the popup
  639. */
  640. public boolean isScrollToSelectedItem() {
  641. return getState(false).scrollToSelectedItem;
  642. }
  643. @Override
  644. public ItemCaptionGenerator<T> getItemCaptionGenerator() {
  645. return super.getItemCaptionGenerator();
  646. }
  647. @Override
  648. public void setItemCaptionGenerator(
  649. ItemCaptionGenerator<T> itemCaptionGenerator) {
  650. super.setItemCaptionGenerator(itemCaptionGenerator);
  651. }
  652. /**
  653. * Sets the style generator that is used to produce custom class names for
  654. * items visible in the popup. The CSS class name that will be added to the
  655. * item is <tt>v-filterselect-item-[style name]</tt>. Returning null from
  656. * the generator results in no custom style name being set.
  657. *
  658. * @see StyleGenerator
  659. *
  660. * @param itemStyleGenerator
  661. * the item style generator to set, not null
  662. * @throws NullPointerException
  663. * if {@code itemStyleGenerator} is {@code null}
  664. * @since 8.0
  665. */
  666. public void setStyleGenerator(StyleGenerator<T> itemStyleGenerator) {
  667. Objects.requireNonNull(itemStyleGenerator,
  668. "Item style generator must not be null");
  669. this.itemStyleGenerator = itemStyleGenerator;
  670. getDataCommunicator().reset();
  671. }
  672. /**
  673. * Gets the currently used style generator that is used to generate CSS
  674. * class names for items. The default item style provider returns null for
  675. * all items, resulting in no custom item class names being set.
  676. *
  677. * @see StyleGenerator
  678. * @see #setStyleGenerator(StyleGenerator)
  679. *
  680. * @return the currently used item style generator, not null
  681. * @since 8.0
  682. */
  683. public StyleGenerator<T> getStyleGenerator() {
  684. return itemStyleGenerator;
  685. }
  686. @Override
  687. public void setItemIconGenerator(IconGenerator<T> itemIconGenerator) {
  688. super.setItemIconGenerator(itemIconGenerator);
  689. }
  690. @Override
  691. public IconGenerator<T> getItemIconGenerator() {
  692. return super.getItemIconGenerator();
  693. }
  694. /**
  695. * Sets the handler that is called when user types a new item. The creation
  696. * of new items is allowed when a new item handler has been set. If new item
  697. * provider is also set, the new item handler is ignored.
  698. *
  699. * @param newItemHandler
  700. * handler called for new items, null to only permit the
  701. * selection of existing items, all options ignored if new item
  702. * provider is set
  703. * @since 8.0
  704. * @deprecated Since 8.4 use {@link #setNewItemProvider(NewItemProvider)}
  705. * instead.
  706. */
  707. @Deprecated
  708. public void setNewItemHandler(NewItemHandler newItemHandler) {
  709. getLogger().log(Level.WARNING,
  710. "NewItemHandler is deprecated. Please use NewItemProvider instead.");
  711. this.newItemHandler = newItemHandler;
  712. getState(true).allowNewItems = newItemProvider != null
  713. || newItemHandler != null;
  714. }
  715. /**
  716. * Sets the provider function that is called when user types a new item. The
  717. * creation of new items is allowed when a new item provider has been set.
  718. * If a deprecated new item handler is also set it is ignored in favor of
  719. * new item provider.
  720. *
  721. * @param newItemProvider
  722. * provider function that is called for new items, null to only
  723. * permit the selection of existing items or to use a deprecated
  724. * new item handler if set
  725. * @since 8.4
  726. */
  727. public void setNewItemProvider(NewItemProvider<T> newItemProvider) {
  728. this.newItemProvider = newItemProvider;
  729. getState(true).allowNewItems = newItemProvider != null
  730. || newItemHandler != null;
  731. }
  732. /**
  733. * Returns the handler called when the user enters a new item (not present
  734. * in the data provider).
  735. *
  736. * @return new item handler or null if none specified
  737. * @deprecated Since 8.4 use {@link #getNewItemProvider()} instead.
  738. */
  739. @Deprecated
  740. public NewItemHandler getNewItemHandler() {
  741. return newItemHandler;
  742. }
  743. /**
  744. * Returns the provider function that is called when the user enters a new
  745. * item (not present in the data provider).
  746. *
  747. * @since 8.4
  748. * @return new item provider or null if none specified
  749. */
  750. public NewItemProvider<T> getNewItemProvider() {
  751. return newItemProvider;
  752. }
  753. // HasValue methods delegated to the selection model
  754. @Override
  755. public Registration addValueChangeListener(
  756. HasValue.ValueChangeListener<T> listener) {
  757. return addSelectionListener(event -> listener
  758. .valueChange(new ValueChangeEvent<>(event.getComponent(), this,
  759. event.getOldValue(), event.isUserOriginated())));
  760. }
  761. @Override
  762. protected ComboBoxState getState() {
  763. return (ComboBoxState) super.getState();
  764. }
  765. @Override
  766. protected ComboBoxState getState(boolean markAsDirty) {
  767. return (ComboBoxState) super.getState(markAsDirty);
  768. }
  769. @Override
  770. protected void updateSelectedItemState(T value) {
  771. super.updateSelectedItemState(value);
  772. updateSelectedItemCaption(value);
  773. updateSelectedItemIcon(value);
  774. }
  775. private void updateSelectedItemCaption(T value) {
  776. String selectedCaption = null;
  777. if (value != null) {
  778. selectedCaption = getItemCaptionGenerator().apply(value);
  779. }
  780. getState().selectedItemCaption = selectedCaption;
  781. }
  782. private void updateSelectedItemIcon(T value) {
  783. String selectedItemIcon = null;
  784. if (value != null) {
  785. Resource icon = getItemIconGenerator().apply(value);
  786. if (icon != null) {
  787. if (icon instanceof ConnectorResource) {
  788. if (!isAttached()) {
  789. // Deferred resource generation.
  790. return;
  791. }
  792. setResource("selected", icon);
  793. }
  794. selectedItemIcon = ResourceReference
  795. .create(icon, ComboBox.this, "selected").getURL();
  796. }
  797. }
  798. getState().selectedItemIcon = selectedItemIcon;
  799. }
  800. @Override
  801. public void attach() {
  802. super.attach();
  803. // Update icon for ConnectorResource
  804. updateSelectedItemIcon(getValue());
  805. }
  806. @Override
  807. protected Element writeItem(Element design, T item, DesignContext context) {
  808. Element element = design.appendElement("option");
  809. String caption = getItemCaptionGenerator().apply(item);
  810. if (caption != null) {
  811. element.html(DesignFormatter.encodeForTextNode(caption));
  812. } else {
  813. element.html(DesignFormatter.encodeForTextNode(item.toString()));
  814. }
  815. element.attr("item", item.toString());
  816. Resource icon = getItemIconGenerator().apply(item);
  817. if (icon != null) {
  818. DesignAttributeHandler.writeAttribute("icon", element.attributes(),
  819. icon, null, Resource.class, context);
  820. }
  821. String style = getStyleGenerator().apply(item);
  822. if (style != null) {
  823. element.attr("style", style);
  824. }
  825. if (isSelected(item)) {
  826. element.attr("selected", true);
  827. }
  828. return element;
  829. }
  830. @Override
  831. protected void readItems(Element design, DesignContext context) {
  832. setStyleGenerator(new DeclarativeStyleGenerator<>(getStyleGenerator()));
  833. super.readItems(design, context);
  834. }
  835. @SuppressWarnings({ "unchecked", "rawtypes" })
  836. @Override
  837. protected T readItem(Element child, Set<T> selected,
  838. DesignContext context) {
  839. T item = super.readItem(child, selected, context);
  840. if (child.hasAttr("style")) {
  841. StyleGenerator<T> styleGenerator = getStyleGenerator();
  842. if (styleGenerator instanceof DeclarativeStyleGenerator) {
  843. ((DeclarativeStyleGenerator) styleGenerator).setStyle(item,
  844. child.attr("style"));
  845. } else {
  846. throw new IllegalStateException(String.format("Don't know how "
  847. + "to set style using current style generator '%s'",
  848. styleGenerator.getClass().getName()));
  849. }
  850. }
  851. return item;
  852. }
  853. @Override
  854. public DataProvider<T, ?> getDataProvider() {
  855. return internalGetDataProvider();
  856. }
  857. @Override
  858. public <C> void setDataProvider(DataProvider<T, C> dataProvider,
  859. SerializableFunction<String, C> filterConverter) {
  860. Objects.requireNonNull(dataProvider, "dataProvider cannot be null");
  861. Objects.requireNonNull(filterConverter,
  862. "filterConverter cannot be null");
  863. SerializableFunction<String, C> convertOrNull = filterText -> {
  864. if (filterText == null || filterText.isEmpty()) {
  865. return null;
  866. }
  867. return filterConverter.apply(filterText);
  868. };
  869. SerializableConsumer<C> providerFilterSlot = internalSetDataProvider(
  870. dataProvider,
  871. convertOrNull.apply(getState(false).currentFilterText));
  872. filterSlot = filter -> providerFilterSlot
  873. .accept(convertOrNull.apply(filter));
  874. // This workaround is done to fix issue #11642 for unpaged comboboxes.
  875. // Data sources for on the client need to be updated after data provider
  876. // refreshAll so that serverside selection works even before the
  877. // dropdown
  878. // is opened. Only done for in-memory data providers for performance
  879. // reasons.
  880. if (dataProvider instanceof InMemoryDataProvider) {
  881. dataProvider.addDataProviderListener(event -> {
  882. if ((!(event instanceof DataChangeEvent.DataRefreshEvent))
  883. && (getPageLength() == 0)) {
  884. getState().forceDataSourceUpdate = true;
  885. }
  886. });
  887. }
  888. }
  889. /**
  890. * Sets a CallbackDataProvider using the given fetch items callback and a
  891. * size callback.
  892. * <p>
  893. * This method is a shorthand for making a {@link CallbackDataProvider} that
  894. * handles a partial {@link com.vaadin.data.provider.Query Query} object.
  895. *
  896. * @param fetchItems
  897. * a callback for fetching items
  898. * @param sizeCallback
  899. * a callback for getting the count of items
  900. *
  901. * @see CallbackDataProvider
  902. * @see #setDataProvider(DataProvider)
  903. */
  904. public void setDataProvider(FetchItemsCallback<T> fetchItems,
  905. SerializableToIntFunction<String> sizeCallback) {
  906. setDataProvider(new CallbackDataProvider<>(
  907. q -> fetchItems.fetchItems(q.getFilter().orElse(""),
  908. q.getOffset(), q.getLimit()),
  909. q -> sizeCallback.applyAsInt(q.getFilter().orElse(""))));
  910. }
  911. /**
  912. * Predicate to check {@link ComboBox} item captions against user typed
  913. * strings.
  914. *
  915. * @see ComboBox#setItems(CaptionFilter, Collection)
  916. * @see ComboBox#setItems(CaptionFilter, Object[])
  917. * @since 8.0
  918. */
  919. @FunctionalInterface
  920. public interface CaptionFilter
  921. extends SerializableBiPredicate<String, String> {
  922. /**
  923. * Check item caption against entered text.
  924. *
  925. * @param itemCaption
  926. * the caption of the item to filter, not {@code null}
  927. * @param filterText
  928. * user entered filter, not {@code null}
  929. * @return {@code true} if item passes the filter and should be listed,
  930. * {@code false} otherwise
  931. */
  932. @Override
  933. public boolean test(String itemCaption, String filterText);
  934. }
  935. private static Logger getLogger() {
  936. return Logger.getLogger(ComboBox.class.getName());
  937. }
  938. }