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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946
  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.v7.ui;
  17. import java.io.Serializable;
  18. import java.util.ArrayList;
  19. import java.util.Collection;
  20. import java.util.LinkedList;
  21. import java.util.List;
  22. import java.util.Map;
  23. import com.vaadin.event.FieldEvents.BlurEvent;
  24. import com.vaadin.event.FieldEvents.BlurListener;
  25. import com.vaadin.event.FieldEvents.FocusEvent;
  26. import com.vaadin.event.FieldEvents.FocusListener;
  27. import com.vaadin.server.PaintException;
  28. import com.vaadin.server.PaintTarget;
  29. import com.vaadin.server.Resource;
  30. import com.vaadin.v7.data.Container;
  31. import com.vaadin.v7.data.util.filter.SimpleStringFilter;
  32. import com.vaadin.v7.event.FieldEvents;
  33. import com.vaadin.v7.shared.ui.combobox.ComboBoxConstants;
  34. import com.vaadin.v7.shared.ui.combobox.ComboBoxState;
  35. import com.vaadin.v7.shared.ui.combobox.FilteringMode;
  36. /**
  37. * A filtering dropdown single-select. Suitable for newItemsAllowed, but it's
  38. * turned of by default to avoid mistakes. Items are filtered based on user
  39. * input, and loaded dynamically ("lazy-loading") from the server. You can turn
  40. * on newItemsAllowed and change filtering mode (and also turn it off), but you
  41. * can not turn on multi-select mode.
  42. *
  43. * @author Vaadin Ltd
  44. *
  45. * @deprecated As of 8.0 replaced by {@link com.vaadin.ui.ComboBox} based on the
  46. * new data binding API
  47. */
  48. @SuppressWarnings("serial")
  49. @Deprecated
  50. public class ComboBox extends AbstractSelect
  51. implements AbstractSelect.Filtering, FieldEvents.BlurNotifier,
  52. FieldEvents.FocusNotifier {
  53. /**
  54. * ItemStyleGenerator can be used to add custom styles to combo box items
  55. * shown in the popup. The CSS class name that will be added to the item
  56. * style names is <tt>v-filterselect-item-[style name]</tt>.
  57. *
  58. * @since 7.5.6
  59. * @see ComboBox#setItemStyleGenerator(ItemStyleGenerator)
  60. */
  61. @Deprecated
  62. public interface ItemStyleGenerator extends Serializable {
  63. /**
  64. * Called by ComboBox when an item is painted.
  65. *
  66. * @param source
  67. * the source combo box
  68. * @param itemId
  69. * The itemId of the item to be painted. Can be
  70. * <code>null</code> if null selection is allowed.
  71. * @return The style name to add to this item. (the CSS class name will
  72. * be v-filterselect-item-[style name]
  73. */
  74. public String getStyle(ComboBox source, Object itemId);
  75. }
  76. private String inputPrompt = null;
  77. /**
  78. * Holds value of property pageLength. 0 disables paging.
  79. */
  80. protected int pageLength = 10;
  81. // Current page when the user is 'paging' trough options
  82. private int currentPage = -1;
  83. private FilteringMode filteringMode = FilteringMode.STARTSWITH;
  84. private String filterstring;
  85. private String prevfilterstring;
  86. /**
  87. * Number of options that pass the filter, excluding the null item if any.
  88. */
  89. private int filteredSize;
  90. /**
  91. * Cache of filtered options, used only by the in-memory filtering system.
  92. */
  93. private List<Object> filteredOptions;
  94. /**
  95. * Flag to indicate that request repaint is called by filter request only
  96. */
  97. private boolean optionRequest;
  98. /**
  99. * True while painting to suppress item set change notifications that could
  100. * be caused by temporary filtering.
  101. */
  102. private boolean isPainting;
  103. /**
  104. * Flag to indicate whether to scroll the selected item visible (select the
  105. * page on which it is) when opening the popup or not. Only applies to
  106. * single select mode.
  107. *
  108. * This requires finding the index of the item, which can be expensive in
  109. * many large lazy loading containers.
  110. */
  111. private boolean scrollToSelectedItem = true;
  112. private String suggestionPopupWidth = null;
  113. /**
  114. * If text input is not allowed, the ComboBox behaves like a pretty
  115. * NativeSelect - the user can not enter any text and clicking the text
  116. * field opens the drop down with options
  117. */
  118. private boolean textInputAllowed = true;
  119. private ItemStyleGenerator itemStyleGenerator = null;
  120. public ComboBox() {
  121. initDefaults();
  122. }
  123. public ComboBox(String caption, Collection<?> options) {
  124. super(caption, options);
  125. initDefaults();
  126. }
  127. public ComboBox(String caption, Container dataSource) {
  128. super(caption, dataSource);
  129. initDefaults();
  130. }
  131. public ComboBox(String caption) {
  132. super(caption);
  133. initDefaults();
  134. }
  135. /**
  136. * Initialize the ComboBox with default settings
  137. */
  138. private void initDefaults() {
  139. setNewItemsAllowed(false);
  140. setImmediate(true);
  141. }
  142. /**
  143. * Gets the current input prompt.
  144. *
  145. * @see #setInputPrompt(String)
  146. * @return the current input prompt, or null if not enabled
  147. */
  148. public String getInputPrompt() {
  149. return inputPrompt;
  150. }
  151. /**
  152. * Sets the input prompt - a textual prompt that is displayed when the
  153. * select would otherwise be empty, to prompt the user for input.
  154. *
  155. * @param inputPrompt
  156. * the desired input prompt, or null to disable
  157. */
  158. public void setInputPrompt(String inputPrompt) {
  159. this.inputPrompt = inputPrompt;
  160. markAsDirty();
  161. }
  162. private boolean isFilteringNeeded() {
  163. return filterstring != null && !filterstring.isEmpty()
  164. && filteringMode != FilteringMode.OFF;
  165. }
  166. @Override
  167. public void paintContent(PaintTarget target) throws PaintException {
  168. isPainting = true;
  169. try {
  170. if (inputPrompt != null) {
  171. target.addAttribute(ComboBoxConstants.ATTR_INPUTPROMPT,
  172. inputPrompt);
  173. }
  174. if (!textInputAllowed) {
  175. target.addAttribute(ComboBoxConstants.ATTR_NO_TEXT_INPUT, true);
  176. }
  177. // clear caption change listeners
  178. getCaptionChangeListener().clear();
  179. // The tab ordering number
  180. if (getTabIndex() != 0) {
  181. target.addAttribute("tabindex", getTabIndex());
  182. }
  183. // If the field is modified, but not committed, set modified
  184. // attribute
  185. if (isModified()) {
  186. target.addAttribute("modified", true);
  187. }
  188. if (isNewItemsAllowed()) {
  189. target.addAttribute("allownewitem", true);
  190. }
  191. boolean needNullSelectOption = false;
  192. if (isNullSelectionAllowed()) {
  193. target.addAttribute("nullselect", true);
  194. needNullSelectOption = (getNullSelectionItemId() == null);
  195. if (!needNullSelectOption) {
  196. target.addAttribute("nullselectitem", true);
  197. }
  198. }
  199. // Constructs selected keys array
  200. String[] selectedKeys = new String[(getValue() == null
  201. && getNullSelectionItemId() == null ? 0 : 1)];
  202. target.addAttribute("pagelength", pageLength);
  203. if (suggestionPopupWidth != null) {
  204. target.addAttribute("suggestionPopupWidth",
  205. suggestionPopupWidth);
  206. }
  207. target.addAttribute("filteringmode", getFilteringMode().toString());
  208. // Paints the options and create array of selected id keys
  209. int keyIndex = 0;
  210. target.startTag("options");
  211. if (currentPage < 0) {
  212. optionRequest = false;
  213. currentPage = 0;
  214. filterstring = "";
  215. }
  216. boolean nullFilteredOut = isFilteringNeeded();
  217. // null option is needed and not filtered out, even if not on
  218. // current
  219. // page
  220. boolean nullOptionVisible = needNullSelectOption
  221. && !nullFilteredOut;
  222. // first try if using container filters is possible
  223. List<?> options = getOptionsWithFilter(nullOptionVisible);
  224. if (null == options) {
  225. // not able to use container filters, perform explicit in-memory
  226. // filtering
  227. options = getFilteredOptions();
  228. filteredSize = options.size();
  229. options = sanitizeList(options, nullOptionVisible);
  230. }
  231. final boolean paintNullSelection = needNullSelectOption
  232. && currentPage == 0 && !nullFilteredOut;
  233. if (paintNullSelection) {
  234. target.startTag("so");
  235. target.addAttribute("caption", "");
  236. target.addAttribute("key", "");
  237. paintItemStyle(target, null);
  238. target.endTag("so");
  239. }
  240. // Paints the available selection options from data source
  241. for (final Object id : options) {
  242. if (!isNullSelectionAllowed() && id != null
  243. && id.equals(getNullSelectionItemId())
  244. && !isSelected(id)) {
  245. continue;
  246. }
  247. // Gets the option attribute values
  248. final String key = itemIdMapper.key(id);
  249. final String caption = getItemCaption(id);
  250. final Resource icon = getItemIcon(id);
  251. getCaptionChangeListener().addNotifierForItem(id);
  252. // Paints the option
  253. target.startTag("so");
  254. if (icon != null) {
  255. target.addAttribute("icon", icon);
  256. }
  257. target.addAttribute("caption", caption);
  258. if (id != null && id.equals(getNullSelectionItemId())) {
  259. target.addAttribute("nullselection", true);
  260. }
  261. target.addAttribute("key", key);
  262. if (keyIndex < selectedKeys.length && isSelected(id)) {
  263. // at most one item can be selected at a time
  264. selectedKeys[keyIndex++] = key;
  265. }
  266. paintItemStyle(target, id);
  267. target.endTag("so");
  268. }
  269. target.endTag("options");
  270. target.addAttribute("totalitems",
  271. size() + (needNullSelectOption ? 1 : 0));
  272. if (filteredSize > 0 || nullOptionVisible) {
  273. target.addAttribute("totalMatches",
  274. filteredSize + (nullOptionVisible ? 1 : 0));
  275. }
  276. // Paint variables
  277. target.addVariable(this, "selected", selectedKeys);
  278. if (getValue() != null && selectedKeys[0] == null) {
  279. // not always available, e.g. scrollToSelectedIndex=false
  280. // Give the caption for selected item still, not to make it look
  281. // like there is no selection at all
  282. target.addAttribute("selectedCaption",
  283. getItemCaption(getValue()));
  284. }
  285. if (isNewItemsAllowed()) {
  286. target.addVariable(this, "newitem", "");
  287. }
  288. target.addVariable(this, "filter", filterstring);
  289. target.addVariable(this, "page", currentPage);
  290. currentPage = -1; // current page is always set by client
  291. optionRequest = true;
  292. } finally {
  293. isPainting = false;
  294. }
  295. }
  296. private void paintItemStyle(PaintTarget target, Object itemId)
  297. throws PaintException {
  298. if (itemStyleGenerator != null) {
  299. String style = itemStyleGenerator.getStyle(this, itemId);
  300. if (style != null && !style.isEmpty()) {
  301. target.addAttribute("style", style);
  302. }
  303. }
  304. }
  305. /**
  306. * Sets whether it is possible to input text into the field or whether the
  307. * field area of the component is just used to show what is selected. By
  308. * disabling text input, the comboBox will work in the same way as a
  309. * {@link NativeSelect}
  310. *
  311. * @see #isTextInputAllowed()
  312. *
  313. * @param textInputAllowed
  314. * true to allow entering text, false to just show the current
  315. * selection
  316. */
  317. public void setTextInputAllowed(boolean textInputAllowed) {
  318. this.textInputAllowed = textInputAllowed;
  319. markAsDirty();
  320. }
  321. /**
  322. * Returns true if the user can enter text into the field to either filter
  323. * the selections or enter a new value if {@link #isNewItemsAllowed()}
  324. * returns true. If text input is disabled, the comboBox will work in the
  325. * same way as a {@link NativeSelect}
  326. *
  327. * @return
  328. */
  329. public boolean isTextInputAllowed() {
  330. return textInputAllowed;
  331. }
  332. @Override
  333. protected ComboBoxState getState() {
  334. return (ComboBoxState) super.getState();
  335. }
  336. /**
  337. * Returns the filtered options for the current page using a container
  338. * filter.
  339. *
  340. * As a size effect, {@link #filteredSize} is set to the total number of
  341. * items passing the filter.
  342. *
  343. * The current container must be {@link Filterable} and {@link Indexed}, and
  344. * the filtering mode must be suitable for container filtering (tested with
  345. * {@link #canUseContainerFilter()}).
  346. *
  347. * Use {@link #getFilteredOptions()} and
  348. * {@link #sanitizeList(List, boolean)} if this is not the case.
  349. *
  350. * @param needNullSelectOption
  351. * @return filtered list of options (may be empty) or null if cannot use
  352. * container filters
  353. */
  354. protected List<?> getOptionsWithFilter(boolean needNullSelectOption) {
  355. Container container = getContainerDataSource();
  356. if (pageLength == 0 && !isFilteringNeeded()) {
  357. // no paging or filtering: return all items
  358. filteredSize = container.size();
  359. assert filteredSize >= 0;
  360. return new ArrayList<Object>(container.getItemIds());
  361. }
  362. if (!(container instanceof Filterable)
  363. || !(container instanceof Indexed)
  364. || getItemCaptionMode() != ITEM_CAPTION_MODE_PROPERTY) {
  365. return null;
  366. }
  367. Filterable filterable = (Filterable) container;
  368. Filter filter = buildFilter(filterstring, filteringMode);
  369. // adding and removing filters leads to extraneous item set
  370. // change events from the underlying container, but the ComboBox does
  371. // not process or propagate them based on the flag filteringContainer
  372. if (filter != null) {
  373. filterable.addContainerFilter(filter);
  374. }
  375. // try-finally to ensure that the filter is removed from container even
  376. // if a exception is thrown...
  377. try {
  378. Indexed indexed = (Indexed) container;
  379. int indexToEnsureInView = -1;
  380. // if not an option request (item list when user changes page), go
  381. // to page with the selected item after filtering if accepted by
  382. // filter
  383. Object selection = getValue();
  384. if (isScrollToSelectedItem() && !optionRequest
  385. && selection != null) {
  386. // ensure proper page
  387. indexToEnsureInView = indexed.indexOfId(selection);
  388. }
  389. filteredSize = container.size();
  390. assert filteredSize >= 0;
  391. currentPage = adjustCurrentPage(currentPage, needNullSelectOption,
  392. indexToEnsureInView, filteredSize);
  393. int first = getFirstItemIndexOnCurrentPage(needNullSelectOption,
  394. filteredSize);
  395. int last = getLastItemIndexOnCurrentPage(needNullSelectOption,
  396. filteredSize, first);
  397. // Compute the number of items to fetch from the indexes given or
  398. // based on the filtered size of the container
  399. int lastItemToFetch = Math.min(last, filteredSize - 1);
  400. int nrOfItemsToFetch = (lastItemToFetch + 1) - first;
  401. List<?> options = indexed.getItemIds(first, nrOfItemsToFetch);
  402. return options;
  403. } finally {
  404. // to the outside, filtering should not be visible
  405. if (filter != null) {
  406. filterable.removeContainerFilter(filter);
  407. }
  408. }
  409. }
  410. /**
  411. * Constructs a filter instance to use when using a Filterable container in
  412. * the <code>ITEM_CAPTION_MODE_PROPERTY</code> mode.
  413. *
  414. * Note that the client side implementation expects the filter string to
  415. * apply to the item caption string it sees, so changing the behavior of
  416. * this method can cause problems.
  417. *
  418. * @param filterString
  419. * @param filteringMode
  420. * @return
  421. */
  422. protected Filter buildFilter(String filterString,
  423. FilteringMode filteringMode) {
  424. Filter filter = null;
  425. if (null != filterString && !"".equals(filterString)) {
  426. switch (filteringMode) {
  427. case OFF:
  428. break;
  429. case STARTSWITH:
  430. filter = new SimpleStringFilter(getItemCaptionPropertyId(),
  431. filterString, true, true);
  432. break;
  433. case CONTAINS:
  434. filter = new SimpleStringFilter(getItemCaptionPropertyId(),
  435. filterString, true, false);
  436. break;
  437. }
  438. }
  439. return filter;
  440. }
  441. @Override
  442. public void containerItemSetChange(Container.ItemSetChangeEvent event) {
  443. if (!isPainting) {
  444. super.containerItemSetChange(event);
  445. }
  446. }
  447. /**
  448. * Makes correct sublist of given list of options.
  449. * <p>
  450. * If paint is not an option request (affected by page or filter change),
  451. * page will be the one where possible selection exists.
  452. * <p>
  453. * Detects proper first and last item in list to return right page of
  454. * options. Also, if the current page is beyond the end of the list, it will
  455. * be adjusted.
  456. * <p>
  457. * Package private only for testing purposes.
  458. *
  459. * @param options
  460. * @param needNullSelectOption
  461. * flag to indicate if nullselect option needs to be taken into
  462. * consideration
  463. */
  464. List<?> sanitizeList(List<?> options, boolean needNullSelectOption) {
  465. int totalRows = options.size() + (needNullSelectOption ? 1 : 0);
  466. if (pageLength != 0 && totalRows > pageLength) {
  467. // options will not fit on one page
  468. int indexToEnsureInView = -1;
  469. // if not an option request (item list when user changes page), go
  470. // to page with the selected item after filtering if accepted by
  471. // filter
  472. Object selection = getValue();
  473. if (isScrollToSelectedItem() && !optionRequest
  474. && selection != null) {
  475. // ensure proper page
  476. indexToEnsureInView = options.indexOf(selection);
  477. }
  478. int size = options.size();
  479. currentPage = adjustCurrentPage(currentPage, needNullSelectOption,
  480. indexToEnsureInView, size);
  481. int first = getFirstItemIndexOnCurrentPage(needNullSelectOption,
  482. size);
  483. int last = getLastItemIndexOnCurrentPage(needNullSelectOption, size,
  484. first);
  485. return options.subList(first, last + 1);
  486. } else {
  487. return options;
  488. }
  489. }
  490. /**
  491. * Returns the index of the first item on the current page. The index is to
  492. * the underlying (possibly filtered) contents. The null item, if any, does
  493. * not have an index but takes up a slot on the first page.
  494. *
  495. * @param needNullSelectOption
  496. * true if a null option should be shown before any other options
  497. * (takes up the first slot on the first page, not counted in
  498. * index)
  499. * @param size
  500. * number of items after filtering (not including the null item,
  501. * if any)
  502. * @return first item to show on the UI (index to the filtered list of
  503. * options, not taking the null item into consideration if any)
  504. */
  505. private int getFirstItemIndexOnCurrentPage(boolean needNullSelectOption,
  506. int size) {
  507. // Not all options are visible, find out which ones are on the
  508. // current "page".
  509. int first = currentPage * pageLength;
  510. if (needNullSelectOption && currentPage > 0) {
  511. first--;
  512. }
  513. return first;
  514. }
  515. /**
  516. * Returns the index of the last item on the current page. The index is to
  517. * the underlying (possibly filtered) contents. If needNullSelectOption is
  518. * true, the null item takes up the first slot on the first page,
  519. * effectively reducing the first page size by one.
  520. *
  521. * @param needNullSelectOption
  522. * true if a null option should be shown before any other options
  523. * (takes up the first slot on the first page, not counted in
  524. * index)
  525. * @param size
  526. * number of items after filtering (not including the null item,
  527. * if any)
  528. * @param first
  529. * index in the filtered view of the first item of the page
  530. * @return index in the filtered view of the last item on the page
  531. */
  532. private int getLastItemIndexOnCurrentPage(boolean needNullSelectOption,
  533. int size, int first) {
  534. // page length usable for non-null items
  535. int effectivePageLength = pageLength
  536. - (needNullSelectOption && (currentPage == 0) ? 1 : 0);
  537. // zero pageLength implies infinite page size
  538. return pageLength == 0 ? size - 1
  539. : Math.min(size - 1, first + effectivePageLength - 1);
  540. }
  541. /**
  542. * Adjusts the index of the current page if necessary: make sure the current
  543. * page is not after the end of the contents, and optionally go to the page
  544. * containg a specific item. There are no side effects but the adjusted page
  545. * index is returned.
  546. *
  547. * @param page
  548. * page number to use as the starting point
  549. * @param needNullSelectOption
  550. * true if a null option should be shown before any other options
  551. * (takes up the first slot on the first page, not counted in
  552. * index)
  553. * @param indexToEnsureInView
  554. * index of an item that should be included on the page (in the
  555. * data set, not counting the null item if any), -1 for none
  556. * @param size
  557. * number of items after filtering (not including the null item,
  558. * if any)
  559. */
  560. private int adjustCurrentPage(int page, boolean needNullSelectOption,
  561. int indexToEnsureInView, int size) {
  562. if (indexToEnsureInView != -1) {
  563. int newPage = (indexToEnsureInView + (needNullSelectOption ? 1 : 0))
  564. / pageLength;
  565. page = newPage;
  566. }
  567. // adjust the current page if beyond the end of the list
  568. if (page * pageLength > size) {
  569. page = (size + (needNullSelectOption ? 1 : 0)) / pageLength;
  570. }
  571. return page;
  572. }
  573. /**
  574. * Filters the options in memory and returns the full filtered list.
  575. *
  576. * This can be less efficient than using container filters, so use
  577. * {@link #getOptionsWithFilter(boolean)} if possible (filterable container
  578. * and suitable item caption mode etc.).
  579. *
  580. * @return
  581. */
  582. protected List<?> getFilteredOptions() {
  583. if (!isFilteringNeeded()) {
  584. prevfilterstring = null;
  585. filteredOptions = new LinkedList<Object>(getItemIds());
  586. return filteredOptions;
  587. }
  588. if (filterstring.equals(prevfilterstring)) {
  589. return filteredOptions;
  590. }
  591. Collection<?> items;
  592. if (prevfilterstring != null
  593. && filterstring.startsWith(prevfilterstring)) {
  594. items = filteredOptions;
  595. } else {
  596. items = getItemIds();
  597. }
  598. prevfilterstring = filterstring;
  599. filteredOptions = new LinkedList<Object>();
  600. for (final Object itemId : items) {
  601. String caption = getItemCaption(itemId);
  602. if (caption == null || caption.equals("")) {
  603. continue;
  604. } else {
  605. caption = caption.toLowerCase(getLocale());
  606. }
  607. switch (filteringMode) {
  608. case CONTAINS:
  609. if (caption.indexOf(filterstring) > -1) {
  610. filteredOptions.add(itemId);
  611. }
  612. break;
  613. case STARTSWITH:
  614. default:
  615. if (caption.startsWith(filterstring)) {
  616. filteredOptions.add(itemId);
  617. }
  618. break;
  619. }
  620. }
  621. return filteredOptions;
  622. }
  623. /**
  624. * Invoked when the value of a variable has changed.
  625. *
  626. * @see com.vaadin.ui.AbstractComponent#changeVariables(java.lang.Object,
  627. * java.util.Map)
  628. */
  629. @Override
  630. public void changeVariables(Object source, Map<String, Object> variables) {
  631. // Not calling super.changeVariables due the history of select
  632. // component hierarchy
  633. // Selection change
  634. if (variables.containsKey("selected")) {
  635. final String[] ka = (String[]) variables.get("selected");
  636. // Single select mode
  637. if (ka.length == 0) {
  638. // Allows deselection only if the deselected item is visible
  639. final Object current = getValue();
  640. final Collection<?> visible = getVisibleItemIds();
  641. if (visible != null && visible.contains(current)) {
  642. setValue(null, true);
  643. }
  644. } else {
  645. final Object id = itemIdMapper.get(ka[0]);
  646. if (id != null && id.equals(getNullSelectionItemId())) {
  647. setValue(null, true);
  648. } else {
  649. setValue(id, true);
  650. }
  651. }
  652. }
  653. String newFilter;
  654. if ((newFilter = (String) variables.get("filter")) != null) {
  655. // this is a filter request
  656. currentPage = ((Integer) variables.get("page")).intValue();
  657. filterstring = newFilter;
  658. if (filterstring != null) {
  659. filterstring = filterstring.toLowerCase(getLocale());
  660. }
  661. requestRepaint();
  662. } else if (isNewItemsAllowed()) {
  663. // New option entered (and it is allowed)
  664. final String newitem = (String) variables.get("newitem");
  665. if (newitem != null && !newitem.isEmpty()) {
  666. getNewItemHandler().addNewItem(newitem);
  667. // rebuild list
  668. filterstring = null;
  669. prevfilterstring = null;
  670. }
  671. }
  672. if (variables.containsKey(FocusEvent.EVENT_ID)) {
  673. fireEvent(new FocusEvent(this));
  674. }
  675. if (variables.containsKey(BlurEvent.EVENT_ID)) {
  676. fireEvent(new BlurEvent(this));
  677. }
  678. }
  679. @Override
  680. public void setFilteringMode(FilteringMode filteringMode) {
  681. this.filteringMode = filteringMode;
  682. }
  683. @Override
  684. public FilteringMode getFilteringMode() {
  685. return filteringMode;
  686. }
  687. @Override
  688. public void addBlurListener(BlurListener listener) {
  689. addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
  690. BlurListener.blurMethod);
  691. }
  692. @Override
  693. public void removeBlurListener(BlurListener listener) {
  694. removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
  695. }
  696. @Override
  697. public void addFocusListener(FocusListener listener) {
  698. addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
  699. FocusListener.focusMethod);
  700. }
  701. @Override
  702. public void removeFocusListener(FocusListener listener) {
  703. removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
  704. }
  705. /**
  706. * ComboBox does not support multi select mode.
  707. *
  708. * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or
  709. * {@link TwinColSelect} instead
  710. * @see com.vaadin.ui.AbstractSelect#setMultiSelect(boolean)
  711. * @throws UnsupportedOperationException
  712. * if trying to activate multiselect mode
  713. */
  714. @Deprecated
  715. @Override
  716. public void setMultiSelect(boolean multiSelect) {
  717. if (multiSelect) {
  718. throw new UnsupportedOperationException(
  719. "Multiselect not supported");
  720. }
  721. }
  722. /**
  723. * ComboBox does not support multi select mode.
  724. *
  725. * @deprecated As of 7.0, use {@link ListSelect}, {@link OptionGroup} or
  726. * {@link TwinColSelect} instead
  727. *
  728. * @see com.vaadin.ui.AbstractSelect#isMultiSelect()
  729. *
  730. * @return false
  731. */
  732. @Deprecated
  733. @Override
  734. public boolean isMultiSelect() {
  735. return false;
  736. }
  737. /**
  738. * Returns the page length of the suggestion popup.
  739. *
  740. * @return the pageLength
  741. */
  742. public int getPageLength() {
  743. return pageLength;
  744. }
  745. /**
  746. * Returns the suggestion pop-up's width as a CSS string.
  747. *
  748. * @see #setPopupWidth
  749. * @since 7.7
  750. */
  751. public String getPopupWidth() {
  752. return suggestionPopupWidth;
  753. }
  754. /**
  755. * Sets the page length for the suggestion popup. Setting the page length to
  756. * 0 will disable suggestion popup paging (all items visible).
  757. *
  758. * @param pageLength
  759. * the pageLength to set
  760. */
  761. public void setPageLength(int pageLength) {
  762. this.pageLength = pageLength;
  763. markAsDirty();
  764. }
  765. /**
  766. * Sets the suggestion pop-up's width as a CSS string. By using relative
  767. * units (e.g. "50%") it's possible to set the popup's width relative to the
  768. * ComboBox itself.
  769. *
  770. * @see #getPopupWidth()
  771. * @since 7.7
  772. * @param width
  773. * the width
  774. */
  775. public void setPopupWidth(String width) {
  776. suggestionPopupWidth = width;
  777. markAsDirty();
  778. }
  779. /**
  780. * Sets whether to scroll the selected item visible (directly open the page
  781. * on which it is) when opening the combo box popup or not. Only applies to
  782. * single select mode.
  783. *
  784. * This requires finding the index of the item, which can be expensive in
  785. * many large lazy loading containers.
  786. *
  787. * @param scrollToSelectedItem
  788. * true to find the page with the selected item when opening the
  789. * selection popup
  790. */
  791. public void setScrollToSelectedItem(boolean scrollToSelectedItem) {
  792. this.scrollToSelectedItem = scrollToSelectedItem;
  793. }
  794. /**
  795. * Returns true if the select should find the page with the selected item
  796. * when opening the popup (single select combo box only).
  797. *
  798. * @see #setScrollToSelectedItem(boolean)
  799. *
  800. * @return true if the page with the selected item will be shown when
  801. * opening the popup
  802. */
  803. public boolean isScrollToSelectedItem() {
  804. return scrollToSelectedItem;
  805. }
  806. /**
  807. * Sets the item style generator that is used to produce custom styles for
  808. * showing items in the popup. The CSS class name that will be added to the
  809. * item style names is <tt>v-filterselect-item-[style name]</tt>.
  810. *
  811. * @param itemStyleGenerator
  812. * the item style generator to set, or <code>null</code> to not
  813. * use any custom item styles
  814. * @since 7.5.6
  815. */
  816. public void setItemStyleGenerator(ItemStyleGenerator itemStyleGenerator) {
  817. this.itemStyleGenerator = itemStyleGenerator;
  818. markAsDirty();
  819. }
  820. /**
  821. * Gets the currently used item style generator.
  822. *
  823. * @return the itemStyleGenerator the currently used item style generator,
  824. * or <code>null</code> if no generator is used
  825. * @since 7.5.6
  826. */
  827. public ItemStyleGenerator getItemStyleGenerator() {
  828. return itemStyleGenerator;
  829. }
  830. }