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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  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.Collection;
  19. import java.util.HashMap;
  20. import java.util.List;
  21. import java.util.Map;
  22. import java.util.Objects;
  23. import java.util.Set;
  24. import java.util.function.Consumer;
  25. import org.jsoup.nodes.Element;
  26. import com.vaadin.data.HasValue;
  27. import com.vaadin.data.Listing;
  28. import com.vaadin.event.FieldEvents;
  29. import com.vaadin.event.FieldEvents.BlurEvent;
  30. import com.vaadin.event.FieldEvents.BlurListener;
  31. import com.vaadin.event.FieldEvents.FocusAndBlurServerRpcDecorator;
  32. import com.vaadin.event.FieldEvents.FocusEvent;
  33. import com.vaadin.event.FieldEvents.FocusListener;
  34. import com.vaadin.server.KeyMapper;
  35. import com.vaadin.server.Resource;
  36. import com.vaadin.server.ResourceReference;
  37. import com.vaadin.server.SerializableBiPredicate;
  38. import com.vaadin.server.data.DataCommunicator;
  39. import com.vaadin.server.data.DataKeyMapper;
  40. import com.vaadin.server.data.DataProvider;
  41. import com.vaadin.shared.Registration;
  42. import com.vaadin.shared.data.DataCommunicatorConstants;
  43. import com.vaadin.shared.ui.combobox.ComboBoxConstants;
  44. import com.vaadin.shared.ui.combobox.ComboBoxServerRpc;
  45. import com.vaadin.shared.ui.combobox.ComboBoxState;
  46. import com.vaadin.ui.declarative.DesignAttributeHandler;
  47. import com.vaadin.ui.declarative.DesignContext;
  48. import com.vaadin.ui.declarative.DesignFormatter;
  49. import elemental.json.Json;
  50. import elemental.json.JsonObject;
  51. /**
  52. * A filtering dropdown single-select. Items are filtered based on user input.
  53. * Supports the creation of new items when a handler is set by the user.
  54. *
  55. * @param <T>
  56. * item (bean) type in ComboBox
  57. * @author Vaadin Ltd
  58. */
  59. @SuppressWarnings("serial")
  60. public class ComboBox<T> extends AbstractSingleSelect<T>
  61. implements HasValue<T>, FieldEvents.BlurNotifier,
  62. FieldEvents.FocusNotifier, Listing<T, DataProvider<T, String>> {
  63. /**
  64. * Handler that adds a new item based on user input when the new items
  65. * allowed mode is active.
  66. */
  67. @FunctionalInterface
  68. public interface NewItemHandler extends Consumer<String>, Serializable {
  69. }
  70. /**
  71. * Item style generator class for declarative support.
  72. * <p>
  73. * Provides a straightforward mapping between an item and its style.
  74. *
  75. * @param <T>
  76. * item type
  77. */
  78. protected static class DeclarativeStyleGenerator<T>
  79. implements StyleGenerator<T> {
  80. private StyleGenerator<T> fallback;
  81. private Map<T, String> styles = new HashMap<>();
  82. public DeclarativeStyleGenerator(StyleGenerator<T> fallback) {
  83. this.fallback = fallback;
  84. }
  85. @Override
  86. public String apply(T item) {
  87. return styles.containsKey(item) ? styles.get(item)
  88. : fallback.apply(item);
  89. }
  90. /**
  91. * Sets a {@code style} for the {@code item}.
  92. *
  93. * @param item
  94. * a data item
  95. * @param style
  96. * a style for the {@code item}
  97. */
  98. protected void setStyle(T item, String style) {
  99. styles.put(item, style);
  100. }
  101. }
  102. private ComboBoxServerRpc rpc = new ComboBoxServerRpc() {
  103. @Override
  104. public void createNewItem(String itemValue) {
  105. // New option entered
  106. if (getNewItemHandler() != null && itemValue != null
  107. && itemValue.length() > 0) {
  108. getNewItemHandler().accept(itemValue);
  109. }
  110. }
  111. @Override
  112. public void setFilter(String filterText) {
  113. getDataCommunicator().setFilter(filterText);
  114. }
  115. };
  116. /**
  117. * Handler for new items entered by the user.
  118. */
  119. private NewItemHandler newItemHandler;
  120. private StyleGenerator<T> itemStyleGenerator = item -> null;
  121. private final SerializableBiPredicate<String, T> defaultFilterMethod = (
  122. text, item) -> getItemCaptionGenerator().apply(item)
  123. .toLowerCase(getLocale())
  124. .contains(text.toLowerCase(getLocale()));
  125. /**
  126. * Constructs an empty combo box without a caption. The content of the combo
  127. * box can be set with {@link #setDataProvider(DataProvider)} or
  128. * {@link #setItems(Collection)}
  129. */
  130. public ComboBox() {
  131. super(new DataCommunicator<T, String>() {
  132. @Override
  133. protected DataKeyMapper<T> createKeyMapper() {
  134. return new KeyMapper<T>() {
  135. @Override
  136. public void remove(T removeobj) {
  137. // never remove keys from ComboBox to support selection
  138. // of items that are not currently visible
  139. }
  140. };
  141. }
  142. });
  143. init();
  144. }
  145. /**
  146. * Constructs an empty combo box, whose content can be set with
  147. * {@link #setDataProvider(DataProvider)} or {@link #setItems(Collection)}.
  148. *
  149. * @param caption
  150. * the caption to show in the containing layout, null for no
  151. * caption
  152. */
  153. public ComboBox(String caption) {
  154. this();
  155. setCaption(caption);
  156. }
  157. /**
  158. * Constructs a combo box with a static in-memory data provider with the
  159. * given options.
  160. *
  161. * @param caption
  162. * the caption to show in the containing layout, null for no
  163. * caption
  164. * @param options
  165. * collection of options, not null
  166. */
  167. public ComboBox(String caption, Collection<T> options) {
  168. this(caption);
  169. setItems(options);
  170. }
  171. /**
  172. * Initialize the ComboBox with default settings and register client to
  173. * server RPC implementation.
  174. */
  175. private void init() {
  176. registerRpc(rpc);
  177. registerRpc(new FocusAndBlurServerRpcDecorator(this, this::fireEvent));
  178. addDataGenerator((T data, JsonObject jsonObject) -> {
  179. jsonObject.put(DataCommunicatorConstants.NAME,
  180. getItemCaptionGenerator().apply(data));
  181. String style = itemStyleGenerator.apply(data);
  182. if (style != null) {
  183. jsonObject.put(ComboBoxConstants.STYLE, style);
  184. }
  185. Resource icon = getItemIconGenerator().apply(data);
  186. if (icon != null) {
  187. String iconUrl = ResourceReference
  188. .create(icon, ComboBox.this, null).getURL();
  189. jsonObject.put(ComboBoxConstants.ICON, iconUrl);
  190. }
  191. });
  192. }
  193. @Override
  194. public void setItems(Collection<T> items) {
  195. DataProvider<T, String> provider = DataProvider.create(items)
  196. .convertFilter(filterText -> item -> defaultFilterMethod
  197. .test(filterText, item));
  198. setDataProvider(provider);
  199. }
  200. @Override
  201. public void setItems(@SuppressWarnings("unchecked") T... items) {
  202. DataProvider<T, String> provider = DataProvider.create(items)
  203. .convertFilter(filterText -> item -> defaultFilterMethod
  204. .test(filterText, item));
  205. setDataProvider(provider);
  206. }
  207. /**
  208. * Sets the data items of this listing and a simple string filter with which
  209. * the item string and the text the user has input are compared.
  210. * <p>
  211. * Note that unlike {@link #setItems(Collection)}, no automatic case
  212. * conversion is performed before the comparison.
  213. *
  214. * @param filterPredicate
  215. * predicate for comparing the item string (first parameter) and
  216. * the filter string (second parameter)
  217. * @param items
  218. * the data items to display
  219. */
  220. public void setItems(
  221. SerializableBiPredicate<String, String> filterPredicate,
  222. Collection<T> items) {
  223. DataProvider<T, String> provider = DataProvider.create(items)
  224. .convertFilter(filterText -> item -> filterPredicate.test(
  225. getItemCaptionGenerator().apply(item), filterText));
  226. setDataProvider(provider);
  227. }
  228. /**
  229. * Sets the data items of this listing and a simple string filter with which
  230. * the item string and the text the user has input are compared.
  231. * <p>
  232. * Note that unlike {@link #setItems(Collection)}, no automatic case
  233. * conversion is performed before the comparison.
  234. *
  235. * @param filterPredicate
  236. * predicate for comparing the item string (first parameter) and
  237. * the filter string (second parameter)
  238. * @param items
  239. * the data items to display
  240. */
  241. public void setItems(
  242. SerializableBiPredicate<String, String> filterPredicate,
  243. @SuppressWarnings("unchecked") T... items) {
  244. DataProvider<T, String> provider = DataProvider.create(items)
  245. .convertFilter(filterText -> item -> filterPredicate.test(
  246. getItemCaptionGenerator().apply(item), filterText));
  247. setDataProvider(provider);
  248. }
  249. /**
  250. * Gets the current placeholder text shown when the combo box would be
  251. * empty.
  252. *
  253. * @see #setPlaceholder(String)
  254. * @return the current placeholder string, or null if not enabled
  255. */
  256. public String getPlaceholder() {
  257. return getState(false).placeholder;
  258. }
  259. /**
  260. * Sets the placeholder string - a textual prompt that is displayed when the
  261. * select would otherwise be empty, to prompt the user for input.
  262. *
  263. * @param placeholder
  264. * the desired placeholder, or null to disable
  265. */
  266. public void setPlaceholder(String placeholder) {
  267. getState().placeholder = placeholder;
  268. }
  269. /**
  270. * Sets whether it is possible to input text into the field or whether the
  271. * field area of the component is just used to show what is selected. By
  272. * disabling text input, the comboBox will work in the same way as a
  273. * {@link NativeSelect}
  274. *
  275. * @see #isTextInputAllowed()
  276. *
  277. * @param textInputAllowed
  278. * true to allow entering text, false to just show the current
  279. * selection
  280. */
  281. public void setTextInputAllowed(boolean textInputAllowed) {
  282. getState().textInputAllowed = textInputAllowed;
  283. }
  284. /**
  285. * Returns true if the user can enter text into the field to either filter
  286. * the selections or enter a new value if {@link #isNewItemsAllowed()}
  287. * returns true. If text input is disabled, the comboBox will work in the
  288. * same way as a {@link NativeSelect}
  289. *
  290. * @return true if text input is allowed
  291. */
  292. public boolean isTextInputAllowed() {
  293. return getState(false).textInputAllowed;
  294. }
  295. @Override
  296. public Registration addBlurListener(BlurListener listener) {
  297. return addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
  298. BlurListener.blurMethod);
  299. }
  300. @Override
  301. @Deprecated
  302. public void removeBlurListener(BlurListener listener) {
  303. removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
  304. }
  305. @Override
  306. public Registration addFocusListener(FocusListener listener) {
  307. return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
  308. FocusListener.focusMethod);
  309. }
  310. @Override
  311. @Deprecated
  312. public void removeFocusListener(FocusListener listener) {
  313. removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
  314. }
  315. /**
  316. * Returns the page length of the suggestion popup.
  317. *
  318. * @return the pageLength
  319. */
  320. public int getPageLength() {
  321. return getState(false).pageLength;
  322. }
  323. /**
  324. * Returns the suggestion pop-up's width as a CSS string.
  325. *
  326. * @see #setPopupWidth
  327. * @since 7.7
  328. * @return explicitly set popup width as CSS size string or null if not set
  329. */
  330. public String getPopupWidth() {
  331. return getState(false).suggestionPopupWidth;
  332. }
  333. /**
  334. * Sets the page length for the suggestion popup. Setting the page length to
  335. * 0 will disable suggestion popup paging (all items visible).
  336. *
  337. * @param pageLength
  338. * the pageLength to set
  339. */
  340. public void setPageLength(int pageLength) {
  341. getState().pageLength = pageLength;
  342. }
  343. /**
  344. * Returns whether the user is allowed to select nothing in the combo box.
  345. *
  346. * @return true if empty selection is allowed, false otherwise
  347. */
  348. public boolean isEmptySelectionAllowed() {
  349. return getState(false).emptySelectionAllowed;
  350. }
  351. /**
  352. * Sets whether the user is allowed to select nothing in the combo box. When
  353. * true, a special empty item is shown to the user.
  354. *
  355. * @param emptySelectionAllowed
  356. * true to allow not selecting anything, false to require
  357. * selection
  358. */
  359. public void setEmptySelectionAllowed(boolean emptySelectionAllowed) {
  360. getState().emptySelectionAllowed = emptySelectionAllowed;
  361. }
  362. /**
  363. * Returns the empty selection caption.
  364. * <p>
  365. * The empty string {@code ""} is the default empty selection caption.
  366. *
  367. * @see #setEmptySelectionAllowed(boolean)
  368. * @see #isEmptySelectionAllowed()
  369. * @see #setEmptySelectionCaption(String)
  370. * @see #isSelected(Object)
  371. * @see #select(Object)
  372. *
  373. * @return the empty selection caption, not {@code null}
  374. */
  375. public String getEmptySelectionCaption() {
  376. return getState(false).emptySelectionCaption;
  377. }
  378. /**
  379. * Sets the empty selection caption.
  380. * <p>
  381. * The empty string {@code ""} is the default empty selection caption.
  382. * <p>
  383. * If empty selection is allowed via the
  384. * {@link #setEmptySelectionAllowed(boolean)} method (it is by default) then
  385. * the empty item will be shown with the given caption.
  386. *
  387. * @param caption
  388. * the caption to set, not {@code null}
  389. * @see #getNullSelectionItemId()
  390. * @see #isSelected(Object)
  391. * @see #select(Object)
  392. */
  393. public void setEmptySelectionCaption(String caption) {
  394. Objects.nonNull(caption);
  395. getState().emptySelectionCaption = caption;
  396. }
  397. /**
  398. * Sets the suggestion pop-up's width as a CSS string. By using relative
  399. * units (e.g. "50%") it's possible to set the popup's width relative to the
  400. * ComboBox itself.
  401. *
  402. * @see #getPopupWidth()
  403. * @since 7.7
  404. * @param width
  405. * the width
  406. */
  407. public void setPopupWidth(String width) {
  408. getState().suggestionPopupWidth = width;
  409. }
  410. /**
  411. * Sets whether to scroll the selected item visible (directly open the page
  412. * on which it is) when opening the combo box popup or not.
  413. *
  414. * This requires finding the index of the item, which can be expensive in
  415. * many large lazy loading containers.
  416. *
  417. * @param scrollToSelectedItem
  418. * true to find the page with the selected item when opening the
  419. * selection popup
  420. */
  421. public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
  422. getState().scrollToSelectedItem = scrollToSelectedItem;
  423. }
  424. /**
  425. * Returns true if the select should find the page with the selected item
  426. * when opening the popup.
  427. *
  428. * @see #setScrollToSelectedItem(boolean)
  429. *
  430. * @return true if the page with the selected item will be shown when
  431. * opening the popup
  432. */
  433. public boolean isScrollToSelectedItem() {
  434. return getState(false).scrollToSelectedItem;
  435. }
  436. @Override
  437. public ItemCaptionGenerator<T> getItemCaptionGenerator() {
  438. return super.getItemCaptionGenerator();
  439. }
  440. @Override
  441. public void setItemCaptionGenerator(
  442. ItemCaptionGenerator<T> itemCaptionGenerator) {
  443. super.setItemCaptionGenerator(itemCaptionGenerator);
  444. }
  445. /**
  446. * Sets the style generator that is used to produce custom class names for
  447. * items visible in the popup. The CSS class name that will be added to the
  448. * item is <tt>v-filterselect-item-[style name]</tt>. Returning null from
  449. * the generator results in no custom style name being set.
  450. *
  451. * @see StyleGenerator
  452. *
  453. * @param itemStyleGenerator
  454. * the item style generator to set, not null
  455. * @throws NullPointerException
  456. * if {@code itemStyleGenerator} is {@code null}
  457. */
  458. public void setStyleGenerator(StyleGenerator<T> itemStyleGenerator) {
  459. Objects.requireNonNull(itemStyleGenerator,
  460. "Item style generator must not be null");
  461. this.itemStyleGenerator = itemStyleGenerator;
  462. getDataCommunicator().reset();
  463. }
  464. /**
  465. * Gets the currently used style generator that is used to generate CSS
  466. * class names for items. The default item style provider returns null for
  467. * all items, resulting in no custom item class names being set.
  468. *
  469. * @see StyleGenerator
  470. * @see #setStyleGenerator(StyleGenerator)
  471. *
  472. * @return the currently used item style generator, not null
  473. */
  474. public StyleGenerator<T> getStyleGenerator() {
  475. return itemStyleGenerator;
  476. }
  477. @Override
  478. public void setItemIconGenerator(IconGenerator<T> itemIconGenerator) {
  479. super.setItemIconGenerator(itemIconGenerator);
  480. }
  481. @Override
  482. public IconGenerator<T> getItemIconGenerator() {
  483. return super.getItemIconGenerator();
  484. }
  485. /**
  486. * Sets the handler that is called when user types a new item. The creation
  487. * of new items is allowed when a new item handler has been set.
  488. *
  489. * @param newItemHandler
  490. * handler called for new items, null to only permit the
  491. * selection of existing items
  492. */
  493. public void setNewItemHandler(NewItemHandler newItemHandler) {
  494. this.newItemHandler = newItemHandler;
  495. getState().allowNewItems = (newItemHandler != null);
  496. markAsDirty();
  497. }
  498. /**
  499. * Returns the handler called when the user enters a new item (not present
  500. * in the data provider).
  501. *
  502. * @return new item handler or null if none specified
  503. */
  504. public NewItemHandler getNewItemHandler() {
  505. return newItemHandler;
  506. }
  507. // HasValue methods delegated to the selection model
  508. @Override
  509. public Registration addValueChangeListener(
  510. HasValue.ValueChangeListener<T> listener) {
  511. return addSelectionChangeListener(event -> {
  512. listener.accept(new ValueChangeEvent<>(event.getComponent(), this,
  513. event.isUserOriginated()));
  514. });
  515. }
  516. @Override
  517. protected ComboBoxState getState() {
  518. return (ComboBoxState) super.getState();
  519. }
  520. @Override
  521. protected ComboBoxState getState(boolean markAsDirty) {
  522. return (ComboBoxState) super.getState(markAsDirty);
  523. }
  524. @Override
  525. protected void doSetSelectedKey(String key) {
  526. super.doSetSelectedKey(key);
  527. String selectedCaption = null;
  528. T value = getDataCommunicator().getKeyMapper().get(key);
  529. if (value != null) {
  530. selectedCaption = getItemCaptionGenerator().apply(value);
  531. }
  532. getState().selectedItemCaption = selectedCaption;
  533. }
  534. @Override
  535. protected Element writeItem(Element design, T item, DesignContext context) {
  536. Element element = design.appendElement("option");
  537. String caption = getItemCaptionGenerator().apply(item);
  538. if (caption != null) {
  539. element.html(DesignFormatter.encodeForTextNode(caption));
  540. } else {
  541. element.html(DesignFormatter.encodeForTextNode(item.toString()));
  542. }
  543. element.attr("item", item.toString());
  544. Resource icon = getItemIconGenerator().apply(item);
  545. if (icon != null) {
  546. DesignAttributeHandler.writeAttribute("icon", element.attributes(),
  547. icon, null, Resource.class, context);
  548. }
  549. String style = getStyleGenerator().apply(item);
  550. if (style != null) {
  551. element.attr("style", style);
  552. }
  553. if (isSelected(item)) {
  554. element.attr("selected", "");
  555. }
  556. return element;
  557. }
  558. @Override
  559. protected List<T> readItems(Element design, DesignContext context) {
  560. setStyleGenerator(new DeclarativeStyleGenerator<>(getStyleGenerator()));
  561. return super.readItems(design, context);
  562. }
  563. @SuppressWarnings({ "unchecked", "rawtypes" })
  564. @Override
  565. protected T readItem(Element child, Set<T> selected,
  566. DesignContext context) {
  567. T item = super.readItem(child, selected, context);
  568. if (child.hasAttr("style")) {
  569. StyleGenerator<T> styleGenerator = getStyleGenerator();
  570. if (styleGenerator instanceof DeclarativeStyleGenerator) {
  571. ((DeclarativeStyleGenerator) styleGenerator).setStyle(item,
  572. child.attr("style"));
  573. } else {
  574. throw new IllegalStateException(String.format(
  575. "Don't know how "
  576. + "to set style using current style generator '%s'",
  577. styleGenerator.getClass().getName()));
  578. }
  579. }
  580. return item;
  581. }
  582. @Override
  583. @SuppressWarnings("unchecked")
  584. public DataProvider<T, String> getDataProvider() {
  585. return (DataProvider<T, String>) internalGetDataProvider();
  586. }
  587. @Override
  588. public void setDataProvider(DataProvider<T, String> dataProvider) {
  589. internalSetDataProvider(dataProvider);
  590. }
  591. @Override
  592. @SuppressWarnings("unchecked")
  593. public DataCommunicator<T, String> getDataCommunicator() {
  594. // Not actually an unsafe cast. DataCommunicator is final and set by
  595. // ComboBox.
  596. return (DataCommunicator<T, String>) super.getDataCommunicator();
  597. }
  598. @Override
  599. protected void setSelectedFromClient(String key) {
  600. super.setSelectedFromClient(key);
  601. /*
  602. * The client side for combo box always expects a state change for
  603. * selectedItemKey after it has sent a selection change. This means that
  604. * we must store a value in the diffstate that guarantees that a new
  605. * value will be sent, regardless of what the value actually is at the
  606. * time when changes are sent.
  607. *
  608. * Keys are always strings (or null), so using a non-string type will
  609. * always trigger a diff mismatch and a resend.
  610. */
  611. updateDiffstate("selectedItemKey", Json.create(0));
  612. }
  613. }