Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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