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

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