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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  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. public Registration addFocusListener(FocusListener listener) {
  302. return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
  303. FocusListener.focusMethod);
  304. }
  305. /**
  306. * Returns the page length of the suggestion popup.
  307. *
  308. * @return the pageLength
  309. */
  310. public int getPageLength() {
  311. return getState(false).pageLength;
  312. }
  313. /**
  314. * Returns the suggestion pop-up's width as a CSS string. By default this
  315. * width is set to "100%".
  316. *
  317. * @see #setPopupWidth
  318. * @since 7.7
  319. * @return explicitly set popup width as CSS size string or null if not set
  320. */
  321. public String getPopupWidth() {
  322. return getState(false).suggestionPopupWidth;
  323. }
  324. /**
  325. * Sets the page length for the suggestion popup. Setting the page length to
  326. * 0 will disable suggestion popup paging (all items visible).
  327. *
  328. * @param pageLength
  329. * the pageLength to set
  330. */
  331. public void setPageLength(int pageLength) {
  332. getState().pageLength = pageLength;
  333. }
  334. /**
  335. * Returns whether the user is allowed to select nothing in the combo box.
  336. *
  337. * @return true if empty selection is allowed, false otherwise
  338. */
  339. public boolean isEmptySelectionAllowed() {
  340. return getState(false).emptySelectionAllowed;
  341. }
  342. /**
  343. * Sets whether the user is allowed to select nothing in the combo box. When
  344. * true, a special empty item is shown to the user.
  345. *
  346. * @param emptySelectionAllowed
  347. * true to allow not selecting anything, false to require
  348. * selection
  349. */
  350. public void setEmptySelectionAllowed(boolean emptySelectionAllowed) {
  351. getState().emptySelectionAllowed = emptySelectionAllowed;
  352. }
  353. /**
  354. * Returns the empty selection caption.
  355. * <p>
  356. * The empty string {@code ""} is the default empty selection caption.
  357. *
  358. * @see #setEmptySelectionAllowed(boolean)
  359. * @see #isEmptySelectionAllowed()
  360. * @see #setEmptySelectionCaption(String)
  361. * @see #isSelected(Object)
  362. * @see #select(Object)
  363. *
  364. * @return the empty selection caption, not {@code null}
  365. */
  366. public String getEmptySelectionCaption() {
  367. return getState(false).emptySelectionCaption;
  368. }
  369. /**
  370. * Sets the empty selection caption.
  371. * <p>
  372. * The empty string {@code ""} is the default empty selection caption.
  373. * <p>
  374. * If empty selection is allowed via the
  375. * {@link #setEmptySelectionAllowed(boolean)} method (it is by default) then
  376. * the empty item will be shown with the given caption.
  377. *
  378. * @param caption
  379. * the caption to set, not {@code null}
  380. * @see #getNullSelectionItemId()
  381. * @see #isSelected(Object)
  382. * @see #select(Object)
  383. */
  384. public void setEmptySelectionCaption(String caption) {
  385. Objects.nonNull(caption);
  386. getState().emptySelectionCaption = caption;
  387. }
  388. /**
  389. * Sets the suggestion pop-up's width as a CSS string. By using relative
  390. * units (e.g. "50%") it's possible to set the popup's width relative to the
  391. * ComboBox itself.
  392. * <p>
  393. * By default this width is set to "100%" so that the pop-up's width is
  394. * equal to the width of the combobox. By setting width to null the pop-up's
  395. * width will automatically expand beyond 100% relative width to fit the
  396. * content of all displayed items.
  397. *
  398. * @see #getPopupWidth()
  399. * @since 7.7
  400. * @param width
  401. * the width
  402. */
  403. public void setPopupWidth(String width) {
  404. getState().suggestionPopupWidth = width;
  405. }
  406. /**
  407. * Sets whether to scroll the selected item visible (directly open the page
  408. * on which it is) when opening the combo box popup or not.
  409. * <p>
  410. * This requires finding the index of the item, which can be expensive in
  411. * many large lazy loading containers.
  412. *
  413. * @param scrollToSelectedItem
  414. * true to find the page with the selected item when opening the
  415. * selection popup
  416. */
  417. public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
  418. getState().scrollToSelectedItem = scrollToSelectedItem;
  419. }
  420. /**
  421. * Returns true if the select should find the page with the selected item
  422. * when opening the popup.
  423. *
  424. * @see #setScrollToSelectedItem(boolean)
  425. *
  426. * @return true if the page with the selected item will be shown when
  427. * opening the popup
  428. */
  429. public boolean isScrollToSelectedItem() {
  430. return getState(false).scrollToSelectedItem;
  431. }
  432. @Override
  433. public ItemCaptionGenerator<T> getItemCaptionGenerator() {
  434. return super.getItemCaptionGenerator();
  435. }
  436. @Override
  437. public void setItemCaptionGenerator(
  438. ItemCaptionGenerator<T> itemCaptionGenerator) {
  439. super.setItemCaptionGenerator(itemCaptionGenerator);
  440. }
  441. /**
  442. * Sets the style generator that is used to produce custom class names for
  443. * items visible in the popup. The CSS class name that will be added to the
  444. * item is <tt>v-filterselect-item-[style name]</tt>. Returning null from
  445. * the generator results in no custom style name being set.
  446. *
  447. * @see StyleGenerator
  448. *
  449. * @param itemStyleGenerator
  450. * the item style generator to set, not null
  451. * @throws NullPointerException
  452. * if {@code itemStyleGenerator} is {@code null}
  453. */
  454. public void setStyleGenerator(StyleGenerator<T> itemStyleGenerator) {
  455. Objects.requireNonNull(itemStyleGenerator,
  456. "Item style generator must not be null");
  457. this.itemStyleGenerator = itemStyleGenerator;
  458. getDataCommunicator().reset();
  459. }
  460. /**
  461. * Gets the currently used style generator that is used to generate CSS
  462. * class names for items. The default item style provider returns null for
  463. * all items, resulting in no custom item class names being set.
  464. *
  465. * @see StyleGenerator
  466. * @see #setStyleGenerator(StyleGenerator)
  467. *
  468. * @return the currently used item style generator, not null
  469. */
  470. public StyleGenerator<T> getStyleGenerator() {
  471. return itemStyleGenerator;
  472. }
  473. @Override
  474. public void setItemIconGenerator(IconGenerator<T> itemIconGenerator) {
  475. super.setItemIconGenerator(itemIconGenerator);
  476. }
  477. @Override
  478. public IconGenerator<T> getItemIconGenerator() {
  479. return super.getItemIconGenerator();
  480. }
  481. /**
  482. * Sets the handler that is called when user types a new item. The creation
  483. * of new items is allowed when a new item handler has been set.
  484. *
  485. * @param newItemHandler
  486. * handler called for new items, null to only permit the
  487. * selection of existing items
  488. */
  489. public void setNewItemHandler(NewItemHandler newItemHandler) {
  490. this.newItemHandler = newItemHandler;
  491. getState().allowNewItems = (newItemHandler != null);
  492. markAsDirty();
  493. }
  494. /**
  495. * Returns the handler called when the user enters a new item (not present
  496. * in the data provider).
  497. *
  498. * @return new item handler or null if none specified
  499. */
  500. public NewItemHandler getNewItemHandler() {
  501. return newItemHandler;
  502. }
  503. // HasValue methods delegated to the selection model
  504. @Override
  505. public Registration addValueChangeListener(
  506. HasValue.ValueChangeListener<T> listener) {
  507. return addSelectionChangeListener(event -> {
  508. listener.accept(new ValueChangeEvent<>(event.getComponent(), this,
  509. event.isUserOriginated()));
  510. });
  511. }
  512. @Override
  513. protected ComboBoxState getState() {
  514. return (ComboBoxState) super.getState();
  515. }
  516. @Override
  517. protected ComboBoxState getState(boolean markAsDirty) {
  518. return (ComboBoxState) super.getState(markAsDirty);
  519. }
  520. @Override
  521. protected void doSetSelectedKey(String key) {
  522. super.doSetSelectedKey(key);
  523. String selectedCaption = null;
  524. T value = getDataCommunicator().getKeyMapper().get(key);
  525. if (value != null) {
  526. selectedCaption = getItemCaptionGenerator().apply(value);
  527. }
  528. getState().selectedItemCaption = selectedCaption;
  529. }
  530. @Override
  531. protected Element writeItem(Element design, T item, DesignContext context) {
  532. Element element = design.appendElement("option");
  533. String caption = getItemCaptionGenerator().apply(item);
  534. if (caption != null) {
  535. element.html(DesignFormatter.encodeForTextNode(caption));
  536. } else {
  537. element.html(DesignFormatter.encodeForTextNode(item.toString()));
  538. }
  539. element.attr("item", item.toString());
  540. Resource icon = getItemIconGenerator().apply(item);
  541. if (icon != null) {
  542. DesignAttributeHandler.writeAttribute("icon", element.attributes(),
  543. icon, null, Resource.class, context);
  544. }
  545. String style = getStyleGenerator().apply(item);
  546. if (style != null) {
  547. element.attr("style", style);
  548. }
  549. if (isSelected(item)) {
  550. element.attr("selected", "");
  551. }
  552. return element;
  553. }
  554. @Override
  555. protected List<T> readItems(Element design, DesignContext context) {
  556. setStyleGenerator(new DeclarativeStyleGenerator<>(getStyleGenerator()));
  557. return super.readItems(design, context);
  558. }
  559. @SuppressWarnings({ "unchecked", "rawtypes" })
  560. @Override
  561. protected T readItem(Element child, Set<T> selected,
  562. DesignContext context) {
  563. T item = super.readItem(child, selected, context);
  564. if (child.hasAttr("style")) {
  565. StyleGenerator<T> styleGenerator = getStyleGenerator();
  566. if (styleGenerator instanceof DeclarativeStyleGenerator) {
  567. ((DeclarativeStyleGenerator) styleGenerator).setStyle(item,
  568. child.attr("style"));
  569. } else {
  570. throw new IllegalStateException(String.format(
  571. "Don't know how "
  572. + "to set style using current style generator '%s'",
  573. styleGenerator.getClass().getName()));
  574. }
  575. }
  576. return item;
  577. }
  578. @Override
  579. @SuppressWarnings("unchecked")
  580. public DataProvider<T, String> getDataProvider() {
  581. return (DataProvider<T, String>) internalGetDataProvider();
  582. }
  583. @Override
  584. public void setDataProvider(DataProvider<T, String> dataProvider) {
  585. internalSetDataProvider(dataProvider);
  586. }
  587. @Override
  588. @SuppressWarnings("unchecked")
  589. public DataCommunicator<T, String> getDataCommunicator() {
  590. // Not actually an unsafe cast. DataCommunicator is final and set by
  591. // ComboBox.
  592. return (DataCommunicator<T, String>) super.getDataCommunicator();
  593. }
  594. @Override
  595. protected void setSelectedFromClient(String key) {
  596. super.setSelectedFromClient(key);
  597. /*
  598. * The client side for combo box always expects a state change for
  599. * selectedItemKey after it has sent a selection change. This means that
  600. * we must store a value in the diffstate that guarantees that a new
  601. * value will be sent, regardless of what the value actually is at the
  602. * time when changes are sent.
  603. *
  604. * Keys are always strings (or null), so using a non-string type will
  605. * always trigger a diff mismatch and a resend.
  606. */
  607. updateDiffstate("selectedItemKey", Json.create(0));
  608. }
  609. }