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

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