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 37KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069
  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. private Registration dataProviderListener = null;
  225. /**
  226. * Constructs an empty combo box without a caption. The content of the combo
  227. * box can be set with {@link #setDataProvider(DataProvider)} or
  228. * {@link #setItems(Collection)}
  229. */
  230. public ComboBox() {
  231. this(new DataCommunicator<T>() {
  232. @Override
  233. protected DataKeyMapper<T> createKeyMapper(
  234. ValueProvider<T, Object> identifierGetter) {
  235. return new KeyMapper<T>(identifierGetter) {
  236. @Override
  237. public void remove(T removeobj) {
  238. // never remove keys from ComboBox to support selection
  239. // of items that are not currently visible
  240. }
  241. };
  242. }
  243. });
  244. }
  245. /**
  246. * Constructs an empty combo box, whose content can be set with
  247. * {@link #setDataProvider(DataProvider)} or {@link #setItems(Collection)}.
  248. *
  249. * @param caption
  250. * the caption to show in the containing layout, null for no
  251. * caption
  252. */
  253. public ComboBox(String caption) {
  254. this();
  255. setCaption(caption);
  256. }
  257. /**
  258. * Constructs a combo box with a static in-memory data provider with the
  259. * given options.
  260. *
  261. * @param caption
  262. * the caption to show in the containing layout, null for no
  263. * caption
  264. * @param options
  265. * collection of options, not null
  266. */
  267. public ComboBox(String caption, Collection<T> options) {
  268. this(caption);
  269. setItems(options);
  270. }
  271. /**
  272. * Constructs and initializes an empty combo box.
  273. *
  274. * @param dataCommunicator
  275. * the data comnunicator to use with this ComboBox
  276. * @since 8.5
  277. */
  278. protected ComboBox(DataCommunicator<T> dataCommunicator) {
  279. super(dataCommunicator);
  280. init();
  281. }
  282. /**
  283. * Initialize the ComboBox with default settings and register client to
  284. * server RPC implementation.
  285. */
  286. private void init() {
  287. registerRpc(rpc);
  288. registerRpc(new FocusAndBlurServerRpcDecorator(this, this::fireEvent));
  289. addDataGenerator(new DataGenerator<T>() {
  290. /**
  291. * Map for storing names for icons.
  292. */
  293. private Map<Object, String> resourceKeyMap = new HashMap<>();
  294. private int counter = 0;
  295. @Override
  296. public void generateData(T item, JsonObject jsonObject) {
  297. String caption = getItemCaptionGenerator().apply(item);
  298. if (caption == null) {
  299. caption = "";
  300. }
  301. jsonObject.put(DataCommunicatorConstants.NAME, caption);
  302. String style = itemStyleGenerator.apply(item);
  303. if (style != null) {
  304. jsonObject.put(ComboBoxConstants.STYLE, style);
  305. }
  306. Resource icon = getItemIcon(item);
  307. if (icon != null) {
  308. String iconKey = resourceKeyMap
  309. .get(getDataProvider().getId(item));
  310. String iconUrl = ResourceReference
  311. .create(icon, ComboBox.this, iconKey).getURL();
  312. jsonObject.put(ComboBoxConstants.ICON, iconUrl);
  313. }
  314. }
  315. @Override
  316. public void destroyData(T item) {
  317. Object itemId = getDataProvider().getId(item);
  318. if (resourceKeyMap.containsKey(itemId)) {
  319. setResource(resourceKeyMap.get(itemId), null);
  320. resourceKeyMap.remove(itemId);
  321. }
  322. }
  323. private Resource getItemIcon(T item) {
  324. Resource icon = getItemIconGenerator().apply(item);
  325. if (icon == null || !(icon instanceof ConnectorResource)) {
  326. return icon;
  327. }
  328. Object itemId = getDataProvider().getId(item);
  329. if (!resourceKeyMap.containsKey(itemId)) {
  330. resourceKeyMap.put(itemId, "icon" + (counter++));
  331. }
  332. setResource(resourceKeyMap.get(itemId), icon);
  333. return icon;
  334. }
  335. });
  336. }
  337. /**
  338. * {@inheritDoc}
  339. * <p>
  340. * Filtering will use a case insensitive match to show all items where the
  341. * filter text is a substring of the caption displayed for that item.
  342. */
  343. @Override
  344. public void setItems(Collection<T> items) {
  345. ListDataProvider<T> listDataProvider = DataProvider.ofCollection(items);
  346. setDataProvider(listDataProvider);
  347. }
  348. /**
  349. * {@inheritDoc}
  350. * <p>
  351. * Filtering will use a case insensitive match to show all items where the
  352. * filter text is a substring of the caption displayed for that item.
  353. */
  354. @Override
  355. public void setItems(Stream<T> streamOfItems) {
  356. // Overridden only to add clarification to javadocs
  357. super.setItems(streamOfItems);
  358. }
  359. /**
  360. * {@inheritDoc}
  361. * <p>
  362. * Filtering will use a case insensitive match to show all items where the
  363. * filter text is a substring of the caption displayed for that item.
  364. */
  365. @Override
  366. public void setItems(T... items) {
  367. // Overridden only to add clarification to javadocs
  368. super.setItems(items);
  369. }
  370. /**
  371. * Sets a list data provider as the data provider of this combo box.
  372. * Filtering will use a case insensitive match to show all items where the
  373. * filter text is a substring of the caption displayed for that item.
  374. * <p>
  375. * Note that this is a shorthand that calls
  376. * {@link #setDataProvider(DataProvider)} with a wrapper of the provided
  377. * list data provider. This means that {@link #getDataProvider()} will
  378. * return the wrapper instead of the original list data provider.
  379. *
  380. * @param listDataProvider
  381. * the list data provider to use, not <code>null</code>
  382. * @since 8.0
  383. */
  384. public void setDataProvider(ListDataProvider<T> listDataProvider) {
  385. // Cannot use the case insensitive contains shorthand from
  386. // ListDataProvider since it wouldn't react to locale changes
  387. CaptionFilter defaultCaptionFilter = (itemText, filterText) -> itemText
  388. .toLowerCase(getLocale())
  389. .contains(filterText.toLowerCase(getLocale()));
  390. setDataProvider(defaultCaptionFilter, listDataProvider);
  391. }
  392. /**
  393. * Sets the data items of this listing and a simple string filter with which
  394. * the item string and the text the user has input are compared.
  395. * <p>
  396. * Note that unlike {@link #setItems(Collection)}, no automatic case
  397. * conversion is performed before the comparison.
  398. *
  399. * @param captionFilter
  400. * filter to check if an item is shown when user typed some text
  401. * into the ComboBox
  402. * @param items
  403. * the data items to display
  404. * @since 8.0
  405. */
  406. public void setItems(CaptionFilter captionFilter, Collection<T> items) {
  407. ListDataProvider<T> listDataProvider = DataProvider.ofCollection(items);
  408. setDataProvider(captionFilter, listDataProvider);
  409. }
  410. /**
  411. * Sets a list data provider with an item caption filter as the data
  412. * provider of this combo box. The caption filter is used to compare the
  413. * displayed caption of each item to the filter text entered by the user.
  414. *
  415. * @param captionFilter
  416. * filter to check if an item is shown when user typed some text
  417. * into the ComboBox
  418. * @param listDataProvider
  419. * the list data provider to use, not <code>null</code>
  420. * @since 8.0
  421. */
  422. public void setDataProvider(CaptionFilter captionFilter,
  423. ListDataProvider<T> listDataProvider) {
  424. Objects.requireNonNull(listDataProvider,
  425. "List data provider cannot be null");
  426. // Must do getItemCaptionGenerator() for each operation since it might
  427. // not be the same as when this method was invoked
  428. setDataProvider(listDataProvider, filterText -> item -> captionFilter
  429. .test(getItemCaptionOfItem(item), filterText));
  430. }
  431. // Helper method for the above to make lambda more readable
  432. private String getItemCaptionOfItem(T item) {
  433. String caption = getItemCaptionGenerator().apply(item);
  434. if (caption == null) {
  435. caption = "";
  436. }
  437. return caption;
  438. }
  439. /**
  440. * Sets the data items of this listing and a simple string filter with which
  441. * the item string and the text the user has input are compared.
  442. * <p>
  443. * Note that unlike {@link #setItems(Collection)}, no automatic case
  444. * conversion is performed before the comparison.
  445. *
  446. * @param captionFilter
  447. * filter to check if an item is shown when user typed some text
  448. * into the ComboBox
  449. * @param items
  450. * the data items to display
  451. * @since 8.0
  452. */
  453. public void setItems(CaptionFilter captionFilter,
  454. @SuppressWarnings("unchecked") T... items) {
  455. setItems(captionFilter, Arrays.asList(items));
  456. }
  457. /**
  458. * Gets the current placeholder text shown when the combo box would be
  459. * empty.
  460. *
  461. * @see #setPlaceholder(String)
  462. * @return the current placeholder string, or null if not enabled
  463. * @since 8.0
  464. */
  465. public String getPlaceholder() {
  466. return getState(false).placeholder;
  467. }
  468. /**
  469. * Sets the placeholder string - a textual prompt that is displayed when the
  470. * select would otherwise be empty, to prompt the user for input.
  471. *
  472. * @param placeholder
  473. * the desired placeholder, or null to disable
  474. * @since 8.0
  475. */
  476. public void setPlaceholder(String placeholder) {
  477. getState().placeholder = placeholder;
  478. }
  479. /**
  480. * Sets whether it is possible to input text into the field or whether the
  481. * field area of the component is just used to show what is selected. By
  482. * disabling text input, the comboBox will work in the same way as a
  483. * {@link NativeSelect}
  484. *
  485. * @see #isTextInputAllowed()
  486. *
  487. * @param textInputAllowed
  488. * true to allow entering text, false to just show the current
  489. * selection
  490. */
  491. public void setTextInputAllowed(boolean textInputAllowed) {
  492. getState().textInputAllowed = textInputAllowed;
  493. }
  494. /**
  495. * Returns true if the user can enter text into the field to either filter
  496. * the selections or enter a new value if new item provider or handler is
  497. * set (see {@link #setNewItemProvider(NewItemProvider)} (recommended) and
  498. * {@link #setNewItemHandler(NewItemHandler)} (deprecated)). If text input
  499. * is disabled, the comboBox will work in the same way as a
  500. * {@link NativeSelect}
  501. *
  502. * @return true if text input is allowed
  503. */
  504. public boolean isTextInputAllowed() {
  505. return getState(false).textInputAllowed;
  506. }
  507. @Override
  508. public Registration addBlurListener(BlurListener listener) {
  509. return addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
  510. BlurListener.blurMethod);
  511. }
  512. @Override
  513. public Registration addFocusListener(FocusListener listener) {
  514. return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
  515. FocusListener.focusMethod);
  516. }
  517. /**
  518. * Returns the page length of the suggestion popup.
  519. *
  520. * @return the pageLength
  521. */
  522. public int getPageLength() {
  523. return getState(false).pageLength;
  524. }
  525. /**
  526. * Returns the suggestion pop-up's width as a CSS string. By default this
  527. * width is set to "100%".
  528. *
  529. * @see #setPopupWidth
  530. * @since 7.7
  531. * @return explicitly set popup width as CSS size string or null if not set
  532. */
  533. public String getPopupWidth() {
  534. return getState(false).suggestionPopupWidth;
  535. }
  536. /**
  537. * Sets the page length for the suggestion popup. Setting the page length to
  538. * 0 will disable suggestion popup paging (all items visible).
  539. *
  540. * @param pageLength
  541. * the pageLength to set
  542. */
  543. public void setPageLength(int pageLength) {
  544. getState().pageLength = pageLength;
  545. }
  546. /**
  547. * Returns whether the user is allowed to select nothing in the combo box.
  548. *
  549. * @return true if empty selection is allowed, false otherwise
  550. * @since 8.0
  551. */
  552. public boolean isEmptySelectionAllowed() {
  553. return getState(false).emptySelectionAllowed;
  554. }
  555. /**
  556. * Sets whether the user is allowed to select nothing in the combo box. When
  557. * true, a special empty item is shown to the user.
  558. *
  559. * @param emptySelectionAllowed
  560. * true to allow not selecting anything, false to require
  561. * selection
  562. * @since 8.0
  563. */
  564. public void setEmptySelectionAllowed(boolean emptySelectionAllowed) {
  565. getState().emptySelectionAllowed = emptySelectionAllowed;
  566. }
  567. /**
  568. * Returns the empty selection caption.
  569. * <p>
  570. * The empty string {@code ""} is the default empty selection caption.
  571. *
  572. * @return the empty selection caption, not {@code null}
  573. * @see #setEmptySelectionAllowed(boolean)
  574. * @see #isEmptySelectionAllowed()
  575. * @see #setEmptySelectionCaption(String)
  576. * @see #isSelected(Object)
  577. * @since 8.0
  578. */
  579. public String getEmptySelectionCaption() {
  580. return getState(false).emptySelectionCaption;
  581. }
  582. /**
  583. * Sets the empty selection caption.
  584. * <p>
  585. * The empty string {@code ""} is the default empty selection caption.
  586. * <p>
  587. * If empty selection is allowed via the
  588. * {@link #setEmptySelectionAllowed(boolean)} method (it is by default) then
  589. * the empty item will be shown with the given caption.
  590. *
  591. * @param caption
  592. * the caption to set, not {@code null}
  593. * @see #isSelected(Object)
  594. * @since 8.0
  595. */
  596. public void setEmptySelectionCaption(String caption) {
  597. Objects.nonNull(caption);
  598. getState().emptySelectionCaption = caption;
  599. }
  600. /**
  601. * Sets the suggestion pop-up's width as a CSS string. By using relative
  602. * units (e.g. "50%") it's possible to set the popup's width relative to the
  603. * ComboBox itself.
  604. * <p>
  605. * By default this width is set to "100%" so that the pop-up's width is
  606. * equal to the width of the combobox. By setting width to null the pop-up's
  607. * width will automatically expand beyond 100% relative width to fit the
  608. * content of all displayed items.
  609. *
  610. * @see #getPopupWidth()
  611. * @since 7.7
  612. * @param width
  613. * the width
  614. */
  615. public void setPopupWidth(String width) {
  616. getState().suggestionPopupWidth = width;
  617. }
  618. /**
  619. * Sets whether to scroll the selected item visible (directly open the page
  620. * on which it is) when opening the combo box popup or not.
  621. * <p>
  622. * This requires finding the index of the item, which can be expensive in
  623. * many large lazy loading containers.
  624. *
  625. * @param scrollToSelectedItem
  626. * true to find the page with the selected item when opening the
  627. * selection popup
  628. */
  629. public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
  630. getState().scrollToSelectedItem = scrollToSelectedItem;
  631. }
  632. /**
  633. * Returns true if the select should find the page with the selected item
  634. * when opening the popup.
  635. *
  636. * @see #setScrollToSelectedItem(boolean)
  637. *
  638. * @return true if the page with the selected item will be shown when
  639. * opening the popup
  640. */
  641. public boolean isScrollToSelectedItem() {
  642. return getState(false).scrollToSelectedItem;
  643. }
  644. @Override
  645. public ItemCaptionGenerator<T> getItemCaptionGenerator() {
  646. return super.getItemCaptionGenerator();
  647. }
  648. @Override
  649. public void setItemCaptionGenerator(
  650. ItemCaptionGenerator<T> itemCaptionGenerator) {
  651. super.setItemCaptionGenerator(itemCaptionGenerator);
  652. }
  653. /**
  654. * Sets the style generator that is used to produce custom class names for
  655. * items visible in the popup. The CSS class name that will be added to the
  656. * item is <tt>v-filterselect-item-[style name]</tt>. Returning null from
  657. * the generator results in no custom style name being set.
  658. *
  659. * @see StyleGenerator
  660. *
  661. * @param itemStyleGenerator
  662. * the item style generator to set, not null
  663. * @throws NullPointerException
  664. * if {@code itemStyleGenerator} is {@code null}
  665. * @since 8.0
  666. */
  667. public void setStyleGenerator(StyleGenerator<T> itemStyleGenerator) {
  668. Objects.requireNonNull(itemStyleGenerator,
  669. "Item style generator must not be null");
  670. this.itemStyleGenerator = itemStyleGenerator;
  671. getDataCommunicator().reset();
  672. }
  673. /**
  674. * Gets the currently used style generator that is used to generate CSS
  675. * class names for items. The default item style provider returns null for
  676. * all items, resulting in no custom item class names being set.
  677. *
  678. * @see StyleGenerator
  679. * @see #setStyleGenerator(StyleGenerator)
  680. *
  681. * @return the currently used item style generator, not null
  682. * @since 8.0
  683. */
  684. public StyleGenerator<T> getStyleGenerator() {
  685. return itemStyleGenerator;
  686. }
  687. @Override
  688. public void setItemIconGenerator(IconGenerator<T> itemIconGenerator) {
  689. super.setItemIconGenerator(itemIconGenerator);
  690. }
  691. @Override
  692. public IconGenerator<T> getItemIconGenerator() {
  693. return super.getItemIconGenerator();
  694. }
  695. /**
  696. * Sets the handler that is called when user types a new item. The creation
  697. * of new items is allowed when a new item handler has been set. If new item
  698. * provider is also set, the new item handler is ignored.
  699. *
  700. * @param newItemHandler
  701. * handler called for new items, null to only permit the
  702. * selection of existing items, all options ignored if new item
  703. * provider is set
  704. * @since 8.0
  705. * @deprecated Since 8.4 use {@link #setNewItemProvider(NewItemProvider)}
  706. * instead.
  707. */
  708. @Deprecated
  709. public void setNewItemHandler(NewItemHandler newItemHandler) {
  710. getLogger().log(Level.WARNING,
  711. "NewItemHandler is deprecated. Please use NewItemProvider instead.");
  712. this.newItemHandler = newItemHandler;
  713. getState(true).allowNewItems = newItemProvider != null
  714. || newItemHandler != null;
  715. }
  716. /**
  717. * Sets the provider function that is called when user types a new item. The
  718. * creation of new items is allowed when a new item provider has been set.
  719. * If a deprecated new item handler is also set it is ignored in favor of
  720. * new item provider.
  721. *
  722. * @param newItemProvider
  723. * provider function that is called for new items, null to only
  724. * permit the selection of existing items or to use a deprecated
  725. * new item handler if set
  726. * @since 8.4
  727. */
  728. public void setNewItemProvider(NewItemProvider<T> newItemProvider) {
  729. this.newItemProvider = newItemProvider;
  730. getState(true).allowNewItems = newItemProvider != null
  731. || newItemHandler != null;
  732. }
  733. /**
  734. * Returns the handler called when the user enters a new item (not present
  735. * in the data provider).
  736. *
  737. * @return new item handler or null if none specified
  738. * @deprecated Since 8.4 use {@link #getNewItemProvider()} instead.
  739. */
  740. @Deprecated
  741. public NewItemHandler getNewItemHandler() {
  742. return newItemHandler;
  743. }
  744. /**
  745. * Returns the provider function that is called when the user enters a new
  746. * item (not present in the data provider).
  747. *
  748. * @since 8.4
  749. * @return new item provider or null if none specified
  750. */
  751. public NewItemProvider<T> getNewItemProvider() {
  752. return newItemProvider;
  753. }
  754. // HasValue methods delegated to the selection model
  755. @Override
  756. public Registration addValueChangeListener(
  757. HasValue.ValueChangeListener<T> listener) {
  758. return addSelectionListener(event -> listener
  759. .valueChange(new ValueChangeEvent<>(event.getComponent(), this,
  760. event.getOldValue(), event.isUserOriginated())));
  761. }
  762. @Override
  763. protected ComboBoxState getState() {
  764. return (ComboBoxState) super.getState();
  765. }
  766. @Override
  767. protected ComboBoxState getState(boolean markAsDirty) {
  768. return (ComboBoxState) super.getState(markAsDirty);
  769. }
  770. @Override
  771. protected void updateSelectedItemState(T value) {
  772. super.updateSelectedItemState(value);
  773. updateSelectedItemCaption(value);
  774. updateSelectedItemIcon(value);
  775. }
  776. private void updateSelectedItemCaption(T value) {
  777. String selectedCaption = null;
  778. if (value != null) {
  779. selectedCaption = getItemCaptionGenerator().apply(value);
  780. }
  781. getState().selectedItemCaption = selectedCaption;
  782. }
  783. private void updateSelectedItemIcon(T value) {
  784. String selectedItemIcon = null;
  785. if (value != null) {
  786. Resource icon = getItemIconGenerator().apply(value);
  787. if (icon != null) {
  788. if (icon instanceof ConnectorResource) {
  789. if (!isAttached()) {
  790. // Deferred resource generation.
  791. return;
  792. }
  793. setResource("selected", icon);
  794. }
  795. selectedItemIcon = ResourceReference
  796. .create(icon, ComboBox.this, "selected").getURL();
  797. }
  798. }
  799. getState().selectedItemIcon = selectedItemIcon;
  800. }
  801. @Override
  802. public void attach() {
  803. super.attach();
  804. // Update icon for ConnectorResource
  805. updateSelectedItemIcon(getValue());
  806. DataProvider<T, ?> dataProvider = getDataProvider();
  807. if (dataProvider != null && dataProviderListener == null) {
  808. setupDataProviderListener(dataProvider);
  809. }
  810. }
  811. @Override
  812. public void detach() {
  813. if (dataProviderListener != null) {
  814. dataProviderListener.remove();
  815. dataProviderListener = null;
  816. }
  817. super.detach();
  818. }
  819. @Override
  820. protected Element writeItem(Element design, T item, DesignContext context) {
  821. Element element = design.appendElement("option");
  822. String caption = getItemCaptionGenerator().apply(item);
  823. if (caption != null) {
  824. element.html(DesignFormatter.encodeForTextNode(caption));
  825. } else {
  826. element.html(DesignFormatter.encodeForTextNode(item.toString()));
  827. }
  828. element.attr("item", item.toString());
  829. Resource icon = getItemIconGenerator().apply(item);
  830. if (icon != null) {
  831. DesignAttributeHandler.writeAttribute("icon", element.attributes(),
  832. icon, null, Resource.class, context);
  833. }
  834. String style = getStyleGenerator().apply(item);
  835. if (style != null) {
  836. element.attr("style", style);
  837. }
  838. if (isSelected(item)) {
  839. element.attr("selected", true);
  840. }
  841. return element;
  842. }
  843. @Override
  844. protected void readItems(Element design, DesignContext context) {
  845. setStyleGenerator(new DeclarativeStyleGenerator<>(getStyleGenerator()));
  846. super.readItems(design, context);
  847. }
  848. @SuppressWarnings({ "unchecked", "rawtypes" })
  849. @Override
  850. protected T readItem(Element child, Set<T> selected,
  851. DesignContext context) {
  852. T item = super.readItem(child, selected, context);
  853. if (child.hasAttr("style")) {
  854. StyleGenerator<T> styleGenerator = getStyleGenerator();
  855. if (styleGenerator instanceof DeclarativeStyleGenerator) {
  856. ((DeclarativeStyleGenerator) styleGenerator).setStyle(item,
  857. child.attr("style"));
  858. } else {
  859. throw new IllegalStateException(String.format("Don't know how "
  860. + "to set style using current style generator '%s'",
  861. styleGenerator.getClass().getName()));
  862. }
  863. }
  864. return item;
  865. }
  866. @Override
  867. public DataProvider<T, ?> getDataProvider() {
  868. if (this.getDataCommunicator() != null) {
  869. return internalGetDataProvider();
  870. }
  871. return null;
  872. }
  873. @Override
  874. public <C> void setDataProvider(DataProvider<T, C> dataProvider,
  875. SerializableFunction<String, C> filterConverter) {
  876. Objects.requireNonNull(dataProvider, "dataProvider cannot be null");
  877. Objects.requireNonNull(filterConverter,
  878. "filterConverter cannot be null");
  879. SerializableFunction<String, C> convertOrNull = filterText -> {
  880. if (filterText == null || filterText.isEmpty()) {
  881. return null;
  882. }
  883. return filterConverter.apply(filterText);
  884. };
  885. SerializableConsumer<C> providerFilterSlot = internalSetDataProvider(
  886. dataProvider,
  887. convertOrNull.apply(getState(false).currentFilterText));
  888. filterSlot = filter -> providerFilterSlot
  889. .accept(convertOrNull.apply(filter));
  890. setupDataProviderListener(dataProvider);
  891. }
  892. private <C> void setupDataProviderListener(
  893. DataProvider<T, C> dataProvider) {
  894. // This workaround is done to fix issue #11642 for unpaged comboboxes.
  895. // Data sources for on the client need to be updated after data provider
  896. // refreshAll so that serverside selection works even before the
  897. // dropdown
  898. // is opened. Only done for in-memory data providers for performance
  899. // reasons.
  900. if (dataProvider instanceof InMemoryDataProvider) {
  901. if (dataProviderListener != null) {
  902. dataProviderListener.remove();
  903. }
  904. dataProviderListener = dataProvider
  905. .addDataProviderListener(event -> {
  906. if ((!(event instanceof DataChangeEvent.DataRefreshEvent))
  907. && (getPageLength() == 0)) {
  908. getState().forceDataSourceUpdate = true;
  909. }
  910. });
  911. }
  912. }
  913. /**
  914. * Sets a CallbackDataProvider using the given fetch items callback and a
  915. * size callback.
  916. * <p>
  917. * This method is a shorthand for making a {@link CallbackDataProvider} that
  918. * handles a partial {@link com.vaadin.data.provider.Query Query} object.
  919. *
  920. * @param fetchItems
  921. * a callback for fetching items
  922. * @param sizeCallback
  923. * a callback for getting the count of items
  924. *
  925. * @see CallbackDataProvider
  926. * @see #setDataProvider(DataProvider)
  927. */
  928. public void setDataProvider(FetchItemsCallback<T> fetchItems,
  929. SerializableToIntFunction<String> sizeCallback) {
  930. setDataProvider(new CallbackDataProvider<>(
  931. q -> fetchItems.fetchItems(q.getFilter().orElse(""),
  932. q.getOffset(), q.getLimit()),
  933. q -> sizeCallback.applyAsInt(q.getFilter().orElse(""))));
  934. }
  935. /**
  936. * Predicate to check {@link ComboBox} item captions against user typed
  937. * strings.
  938. *
  939. * @see ComboBox#setItems(CaptionFilter, Collection)
  940. * @see ComboBox#setItems(CaptionFilter, Object[])
  941. * @since 8.0
  942. */
  943. @FunctionalInterface
  944. public interface CaptionFilter
  945. extends SerializableBiPredicate<String, String> {
  946. /**
  947. * Check item caption against entered text.
  948. *
  949. * @param itemCaption
  950. * the caption of the item to filter, not {@code null}
  951. * @param filterText
  952. * user entered filter, not {@code null}
  953. * @return {@code true} if item passes the filter and should be listed,
  954. * {@code false} otherwise
  955. */
  956. @Override
  957. public boolean test(String itemCaption, String filterText);
  958. }
  959. private static Logger getLogger() {
  960. return Logger.getLogger(ComboBox.class.getName());
  961. }
  962. }