Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

ComboBox.java 29KB

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