extends Serializable {
93 |
94 | /**
95 | * Returns a stream of items that match the given filter, limiting the results with given offset and limit.
96 | *
97 | * This method is called after the size of the data set is asked from a related size callback. The offset and limit are promised to be within the size
98 | * of the data set.
99 | *
100 | * @param filter a non-null filter string
101 | * @param offset the first index to fetch
102 | * @param limit the fetched item count
103 | * @return stream of items
104 | */
105 | public Stream fetchItems(String filter, int offset, int limit);
106 | }
107 |
108 | /**
109 | * Handler that adds a new item based on user input when the new items allowed mode is active.
110 | *
111 | * @since 8.0
112 | */
113 | @FunctionalInterface
114 | public interface NewItemHandler extends SerializableConsumer {
115 | }
116 |
117 | /**
118 | * Generator that handles the value of the textfield when not selected.
119 | *
120 | * @since 8.0
121 | */
122 | @FunctionalInterface
123 | public interface InputTextFieldCaptionGenerator extends SerializableFunction, String> {
124 | }
125 |
126 | /**
127 | * Item style generator class for declarative support.
128 | *
129 | * Provides a straightforward mapping between an item and its style.
130 | *
131 | * @param item type
132 | * @since 8.0
133 | */
134 | protected static class DeclarativeStyleGenerator implements StyleGenerator {
135 |
136 | private final StyleGenerator fallback;
137 | private final Map styles = new HashMap<>();
138 |
139 | public DeclarativeStyleGenerator(final StyleGenerator fallback) {
140 | this.fallback = fallback;
141 | }
142 |
143 | @Override
144 | public String apply(final T item) {
145 | return this.styles.containsKey(item) ? this.styles.get(item) : this.fallback.apply(item);
146 | }
147 |
148 | /**
149 | * Sets a {@code style} for the {@code item}.
150 | *
151 | * @param item a data item
152 | * @param style a style for the {@code item}
153 | */
154 | protected void setStyle(final T item, final String style) {
155 | this.styles.put(item, style);
156 | }
157 | }
158 |
159 | private final ComboBoxMultiselectServerRpc rpc = new ComboBoxMultiselectServerRpc() {
160 | @Override
161 | public void createNewItem(final String itemValue) {
162 | // New option entered
163 | if (getNewItemHandler() != null && itemValue != null && itemValue.length() > 0) {
164 | getNewItemHandler().accept(itemValue);
165 | }
166 | }
167 |
168 | @Override
169 | public void setFilter(final String filterText) {
170 | ComboBoxMultiselect.this.currentFilterText = filterText;
171 | ComboBoxMultiselect.this.filterSlot.accept(filterText);
172 | }
173 |
174 | @Override
175 | public void updateSelection(final Set selectedItemKeys, final Set deselectedItemKeys, final boolean sortingNeeded) {
176 | ComboBoxMultiselect.this.updateSelection(getItemsForSelectionChange(selectedItemKeys), getItemsForSelectionChange(deselectedItemKeys), true,
177 | sortingNeeded);
178 | }
179 |
180 | private Set getItemsForSelectionChange(final Set keys) {
181 | return keys.stream()
182 | .map(key -> getItemForSelectionChange(key))
183 | .filter(Optional::isPresent)
184 | .map(Optional::get)
185 | .collect(Collectors.toSet());
186 | }
187 |
188 | private Optional getItemForSelectionChange(final String key) {
189 | final T item = getDataCommunicator().getKeyMapper()
190 | .get(key);
191 | if (item == null || !getItemEnabledProvider().test(item)) {
192 | return Optional.empty();
193 | }
194 |
195 | return Optional.of(item);
196 | }
197 |
198 | @Override
199 | public void blur() {
200 | ComboBoxMultiselect.this.sortingSelection = Collections.unmodifiableCollection(getSelectedItems());
201 | setFilter("");
202 | getDataProvider().refreshAll();
203 | }
204 |
205 | @Override
206 | public void selectAll(final String filter) {
207 | final ListDataProvider listDataProvider = ((ListDataProvider) getDataProvider());
208 | final Set addedItems = listDataProvider.getItems()
209 | .stream()
210 | .filter(t -> {
211 | final String caption = getItemCaptionGenerator().apply(t);
212 | if (t == null) {
213 | return false;
214 | }
215 | return caption.toLowerCase()
216 | .contains(filter.toLowerCase());
217 | })
218 | .map(t -> itemToKey(t))
219 | .collect(Collectors.toSet());
220 | updateSelection(addedItems, new HashSet<>(), true);
221 | }
222 |
223 | @Override
224 | public void clear(final String filter) {
225 | final ListDataProvider listDataProvider = ((ListDataProvider) getDataProvider());
226 | final Set removedItems = listDataProvider.getItems()
227 | .stream()
228 | .filter(t -> {
229 | final String caption = getItemCaptionGenerator().apply(t);
230 | if (t == null) {
231 | return false;
232 | }
233 | return caption.toLowerCase()
234 | .contains(filter.toLowerCase());
235 | })
236 | .map(t -> itemToKey(t))
237 | .collect(Collectors.toSet());
238 | updateSelection(new HashSet<>(), removedItems, true);
239 | };
240 | };
241 |
242 | /**
243 | * Handler for new items entered by the user.
244 | */
245 | private NewItemHandler newItemHandler;
246 |
247 | private StyleGenerator itemStyleGenerator = item -> null;
248 |
249 | private String currentFilterText;
250 |
251 | private SerializableConsumer filterSlot = filter -> {
252 | // Just ignore when neither setDataProvider nor setItems has been called
253 | };
254 |
255 | private final InputTextFieldCaptionGenerator inputTextFieldCaptionGenerator = items -> {
256 | if (items.isEmpty()) {
257 | return "";
258 | }
259 |
260 | final List captions = new ArrayList<>();
261 |
262 | if (getState().selectedItemKeys != null) {
263 | for (final T item : items) {
264 | if (item != null) {
265 | captions.add(getItemCaptionGenerator().apply(item));
266 | }
267 | }
268 | }
269 |
270 | return "(" + captions.size() + ") " + StringUtils.join(captions, "; ");
271 | };
272 |
273 | private Collection sortingSelection = Collections.unmodifiableCollection(new ArrayList<>());
274 |
275 | /**
276 | * Constructs an empty combo box without a caption. The content of the combo box can be set with {@link #setDataProvider(DataProvider)} or
277 | * {@link #setItems(Collection)}
278 | */
279 | public ComboBoxMultiselect() {
280 | super();
281 |
282 | init();
283 | }
284 |
285 | /**
286 | * Constructs an empty combo box, whose content can be set with {@link #setDataProvider(DataProvider)} or {@link #setItems(Collection)}.
287 | *
288 | * @param caption the caption to show in the containing layout, null for no caption
289 | */
290 | public ComboBoxMultiselect(final String caption) {
291 | this();
292 | setCaption(caption);
293 | }
294 |
295 | /**
296 | * Constructs a combo box with a static in-memory data provider with the given options.
297 | *
298 | * @param caption the caption to show in the containing layout, null for no caption
299 | * @param options collection of options, not null
300 | */
301 | public ComboBoxMultiselect(final String caption, final Collection options) {
302 | this(caption);
303 |
304 | setItems(options);
305 | }
306 |
307 | /**
308 | * Initialize the ComboBoxMultiselect with default settings and register client to server RPC implementation.
309 | */
310 | private void init() {
311 | registerRpc(this.rpc);
312 | registerRpc(new FocusAndBlurServerRpcDecorator(this, this::fireEvent));
313 |
314 | addDataGenerator((final T data, final JsonObject jsonObject) -> {
315 | String caption = getItemCaptionGenerator().apply(data);
316 | if (caption == null) {
317 | caption = "";
318 | }
319 | jsonObject.put(DataCommunicatorConstants.NAME, caption);
320 | final String style = this.itemStyleGenerator.apply(data);
321 | if (style != null) {
322 | jsonObject.put(ComboBoxMultiselectConstants.STYLE, style);
323 | }
324 | final Resource icon = getItemIconGenerator().apply(data);
325 | if (icon != null) {
326 | final String iconUrl = ResourceReference.create(icon, ComboBoxMultiselect.this, null)
327 | .getURL();
328 | jsonObject.put(ComboBoxMultiselectConstants.ICON, iconUrl);
329 | }
330 | });
331 | }
332 |
333 | /**
334 | * {@inheritDoc}
335 | *
336 | * Filtering will use a case insensitive match to show all items where the filter text is a substring of the caption displayed for that item.
337 | */
338 | @Override
339 | public void setItems(final Collection items) {
340 | final ListDataProvider listDataProvider = DataProvider.ofCollection(items);
341 |
342 | setDataProvider(listDataProvider);
343 |
344 | // sets the PageLength to 10.
345 | // if there are less then 10 items in the combobox, PageLength will get the amount of items.
346 | setPageLength(getDataProvider().size(new Query<>()) >= ComboBoxMultiselect.DEFAULT_PAGE_LENGTH ? ComboBoxMultiselect.DEFAULT_PAGE_LENGTH : getDataProvider().size(new Query<>()));
347 | }
348 |
349 | /**
350 | * {@inheritDoc}
351 | *
352 | * Filtering will use a case insensitive match to show all items where the filter text is a substring of the caption displayed for that item.
353 | */
354 | @Override
355 | public void setItems(final Stream streamOfItems) {
356 | // Overridden only to add clarification to javadocs
357 | super.setItems(streamOfItems);
358 | }
359 |
360 | /**
361 | * {@inheritDoc}
362 | *
363 | * Filtering will use a case insensitive match to show all items where the filter text is a substring of the caption displayed for that item.
364 | */
365 | @Override
366 | public void setItems(@SuppressWarnings("unchecked") final T... items) {
367 | // Overridden only to add clarification to javadocs
368 | super.setItems(items);
369 | }
370 |
371 | /**
372 | * Sets a list data provider as the data provider of this combo box. Filtering will use a case insensitive match to show all items where the filter text is
373 | * a substring of the caption displayed for that item.
374 | *
375 | * Note that this is a shorthand that calls {@link #setDataProvider(DataProvider)} with a wrapper of the provided list data provider. This means that
376 | * {@link #getDataProvider()} will return the wrapper instead of the original list data provider.
377 | *
378 | * @param listDataProvider the list data provider to use, not null
379 | * @since 8.0
380 | */
381 | public void setDataProvider(final ListDataProvider listDataProvider) {
382 | // Cannot use the case insensitive contains shorthand from
383 | // ListDataProvider since it wouldn't react to locale changes
384 | final CaptionFilter defaultCaptionFilter = (itemText, filterText) -> itemText.toLowerCase(getLocale())
385 | .contains(filterText.toLowerCase(getLocale()));
386 |
387 | setDataProvider(defaultCaptionFilter, listDataProvider);
388 | }
389 |
390 | /**
391 | * Sets the data items of this listing and a simple string filter with which the item string and the text the user has input are compared.
392 | *
393 | * Note that unlike {@link #setItems(Collection)}, no automatic case conversion is performed before the comparison.
394 | *
395 | * @param captionFilter filter to check if an item is shown when user typed some text into the ComboBoxMultiselect
396 | * @param items the data items to display
397 | * @since 8.0
398 | */
399 | public void setItems(final CaptionFilter captionFilter, final Collection items) {
400 | final ListDataProvider listDataProvider = DataProvider.ofCollection(items);
401 |
402 | setDataProvider(captionFilter, listDataProvider);
403 | }
404 |
405 | /**
406 | * Sets a list data provider with an item caption filter as the data provider of this combo box. The caption filter is used to compare the displayed caption
407 | * of each item to the filter text entered by the user.
408 | *
409 | * @param captionFilter filter to check if an item is shown when user typed some text into the ComboBoxMultiselect
410 | * @param listDataProvider the list data provider to use, not null
411 | * @since 8.0
412 | */
413 | public void setDataProvider(final CaptionFilter captionFilter, final ListDataProvider listDataProvider) {
414 | Objects.requireNonNull(listDataProvider, "List data provider cannot be null");
415 |
416 | // Must do getItemCaptionGenerator() for each operation since it might
417 | // not be the same as when this method was invoked
418 | setDataProvider(listDataProvider, filterText -> item -> captionFilter.test(getItemCaptionGenerator().apply(item), filterText));
419 | }
420 |
421 | /**
422 | * Sets the data items of this listing and a simple string filter with which the item string and the text the user has input are compared.
423 | *
424 | * Note that unlike {@link #setItems(Collection)}, no automatic case conversion is performed before the comparison.
425 | *
426 | * @param captionFilter filter to check if an item is shown when user typed some text into the ComboBoxMultiselect
427 | * @param items the data items to display
428 | * @since 8.0
429 | */
430 | public void setItems(final CaptionFilter captionFilter, @SuppressWarnings("unchecked") final T... items) {
431 | setItems(captionFilter, Arrays.asList(items));
432 | }
433 |
434 | /**
435 | * Gets the current placeholder text shown when the combo box would be empty.
436 | *
437 | * @see #setPlaceholder(String)
438 | * @return the current placeholder string, or null if not enabled
439 | * @since 8.0
440 | */
441 | public String getPlaceholder() {
442 | return getState(false).placeholder;
443 | }
444 |
445 | /**
446 | * Sets the placeholder string - a textual prompt that is displayed when the select would otherwise be empty, to prompt the user for input.
447 | *
448 | * @param placeholder the desired placeholder, or null to disable
449 | * @since 8.0
450 | */
451 | public void setPlaceholder(final String placeholder) {
452 | getState().placeholder = placeholder;
453 | }
454 |
455 | /**
456 | * Sets whether it is possible to input text into the field or whether the field area of the component is just used to show what is selected. By disabling
457 | * text input, the comboBox will work in the same way as a {@link NativeSelect}
458 | *
459 | * @see #isTextInputAllowed()
460 | *
461 | * @param textInputAllowed true to allow entering text, false to just show the current selection
462 | */
463 | public void setTextInputAllowed(final boolean textInputAllowed) {
464 | getState().textInputAllowed = textInputAllowed;
465 | }
466 |
467 | /**
468 | * Returns true if the user can enter text into the field to either filter the selections or enter a new value if new item handler is set (see
469 | * {@link #setNewItemHandler(NewItemHandler)}. If text input is disabled, the comboBox will work in the same way as a {@link NativeSelect}
470 | *
471 | * @return true if text input is allowed
472 | */
473 | public boolean isTextInputAllowed() {
474 | return getState(false).textInputAllowed;
475 | }
476 |
477 | @Override
478 | public Registration addBlurListener(final BlurListener listener) {
479 | return addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener, BlurListener.blurMethod);
480 | }
481 |
482 | @Override
483 | public Registration addFocusListener(final FocusListener listener) {
484 | return addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener, FocusListener.focusMethod);
485 | }
486 |
487 | /**
488 | * Returns the page length of the suggestion popup.
489 | *
490 | * @return the pageLength
491 | */
492 | public int getPageLength() {
493 | return getState(false).pageLength;
494 | }
495 |
496 | /**
497 | * Returns the suggestion pop-up's width as a CSS string. By default this width is set to "100%".
498 | *
499 | * @see #setPopupWidth
500 | * @since 7.7
501 | * @return explicitly set popup width as CSS size string or null if not set
502 | */
503 | public String getPopupWidth() {
504 | return getState(false).suggestionPopupWidth;
505 | }
506 |
507 | /**
508 | * Sets the page length for the suggestion popup. Setting the page length to 0 will disable suggestion popup paging (all items visible).
509 | *
510 | * @param pageLength the pageLength to set
511 | */
512 | public void setPageLength(final int pageLength) {
513 | getState().pageLength = pageLength;
514 | }
515 |
516 | /**
517 | * Sets the suggestion pop-up's width as a CSS string. By using relative units (e.g. "50%") it's possible to set the popup's width relative to the
518 | * ComboBoxMultiselect itself.
519 | *
520 | * By default this width is set to "100%" so that the pop-up's width is equal to the width of the combobox. By setting width to null the pop-up's width will
521 | * automatically expand beyond 100% relative width to fit the content of all displayed items.
522 | *
523 | * @see #getPopupWidth()
524 | * @since 7.7
525 | * @param width the width
526 | */
527 | public void setPopupWidth(final String width) {
528 | getState().suggestionPopupWidth = width;
529 | }
530 |
531 | /**
532 | * Sets whether to scroll the selected item visible (directly open the page on which it is) when opening the combo box popup or not.
533 | *
534 | * This requires finding the index of the item, which can be expensive in many large lazy loading containers.
535 | *
536 | * @param scrollToSelectedItem true to find the page with the selected item when opening the selection popup
537 | */
538 | public void setScrollToSelectedItem(final boolean scrollToSelectedItem) {
539 | getState().scrollToSelectedItem = scrollToSelectedItem;
540 | }
541 |
542 | /**
543 | * Returns true if the select should find the page with the selected item when opening the popup.
544 | *
545 | * @see #setScrollToSelectedItem(boolean)
546 | *
547 | * @return true if the page with the selected item will be shown when opening the popup
548 | */
549 | public boolean isScrollToSelectedItem() {
550 | return getState(false).scrollToSelectedItem;
551 | }
552 |
553 | /**
554 | * Sets the style generator that is used to produce custom class names for items visible in the popup. The CSS class name that will be added to the item is
555 | * v-filterselect-item-[style name]. Returning null from the generator results in no custom style name being set.
556 | *
557 | * @see StyleGenerator
558 | *
559 | * @param itemStyleGenerator the item style generator to set, not null
560 | * @throws NullPointerException if {@code itemStyleGenerator} is {@code null}
561 | * @since 8.0
562 | */
563 | public void setStyleGenerator(final StyleGenerator itemStyleGenerator) {
564 | Objects.requireNonNull(itemStyleGenerator, "Item style generator must not be null");
565 | this.itemStyleGenerator = itemStyleGenerator;
566 | getDataCommunicator().reset();
567 | }
568 |
569 | /**
570 | * Gets the currently used style generator that is used to generate CSS class names for items. The default item style provider returns null for all items,
571 | * resulting in no custom item class names being set.
572 | *
573 | * @see StyleGenerator
574 | * @see #setStyleGenerator(StyleGenerator)
575 | *
576 | * @return the currently used item style generator, not null
577 | * @since 8.0
578 | */
579 | public StyleGenerator getStyleGenerator() {
580 | return this.itemStyleGenerator;
581 | }
582 |
583 | @Override
584 | public void setItemIconGenerator(final IconGenerator itemIconGenerator) {
585 | super.setItemIconGenerator(itemIconGenerator);
586 | }
587 |
588 | /**
589 | * Sets the handler that is called when user types a new item. The creation of new items is allowed when a new item handler has been set.
590 | *
591 | * @param newItemHandler handler called for new items, null to only permit the selection of existing items
592 | * @since 8.0
593 | */
594 | public void setNewItemHandler(final NewItemHandler newItemHandler) {
595 | this.newItemHandler = newItemHandler;
596 | getState().allowNewItems = newItemHandler != null;
597 | markAsDirty();
598 | }
599 |
600 | /**
601 | * Returns the handler called when the user enters a new item (not present in the data provider).
602 | *
603 | * @return new item handler or null if none specified
604 | */
605 | public NewItemHandler getNewItemHandler() {
606 | return this.newItemHandler;
607 | }
608 |
609 | // HasValue methods delegated to the selection model
610 |
611 | @Override
612 | public Registration addValueChangeListener(final HasValue.ValueChangeListener> listener) {
613 | return addSelectionListener(event -> listener
614 | .valueChange(new ValueChangeEvent<>(event.getComponent(), this, event.getOldValue(), event.isUserOriginated())));
615 | }
616 |
617 | @Override
618 | protected ComboBoxMultiselectState getState() {
619 | return (ComboBoxMultiselectState) super.getState();
620 | }
621 |
622 | @Override
623 | protected ComboBoxMultiselectState getState(final boolean markAsDirty) {
624 | return (ComboBoxMultiselectState) super.getState(markAsDirty);
625 | }
626 |
627 | @Override
628 | protected Element writeItem(final Element design, final T item, final DesignContext context) {
629 | final Element element = design.appendElement("option");
630 |
631 | final String caption = getItemCaptionGenerator().apply(item);
632 | if (caption != null) {
633 | element.html(DesignFormatter.encodeForTextNode(caption));
634 | }
635 | else {
636 | element.html(DesignFormatter.encodeForTextNode(item.toString()));
637 | }
638 | element.attr("item", item.toString());
639 |
640 | final Resource icon = getItemIconGenerator().apply(item);
641 | if (icon != null) {
642 | DesignAttributeHandler.writeAttribute("icon", element.attributes(), icon, null, Resource.class, context);
643 | }
644 |
645 | final String style = getStyleGenerator().apply(item);
646 | if (style != null) {
647 | element.attr("style", style);
648 | }
649 |
650 | if (isSelected(item)) {
651 | element.attr("selected", "");
652 | }
653 |
654 | return element;
655 | }
656 |
657 | @Override
658 | protected void readItems(final Element design, final DesignContext context) {
659 | setStyleGenerator(new DeclarativeStyleGenerator<>(getStyleGenerator()));
660 | super.readItems(design, context);
661 | }
662 |
663 | @SuppressWarnings({ "unchecked", "rawtypes" })
664 | @Override
665 | protected T readItem(final Element child, final Set selected, final DesignContext context) {
666 | final T item = super.readItem(child, selected, context);
667 |
668 | if (child.hasAttr("style")) {
669 | final StyleGenerator styleGenerator = getStyleGenerator();
670 | if (styleGenerator instanceof DeclarativeStyleGenerator) {
671 | ((DeclarativeStyleGenerator) styleGenerator).setStyle(item, child.attr("style"));
672 | }
673 | else {
674 | throw new IllegalStateException(String.format("Don't know how " + "to set style using current style generator '%s'", styleGenerator.getClass()
675 | .getName()));
676 | }
677 | }
678 | return item;
679 | }
680 |
681 | @Override
682 | public DataProvider getDataProvider() {
683 | return internalGetDataProvider();
684 | }
685 |
686 | @Override
687 | public void setDataProvider(final DataProvider dataProvider, final SerializableFunction filterConverter) {
688 | Objects.requireNonNull(dataProvider, "dataProvider cannot be null");
689 | Objects.requireNonNull(filterConverter, "filterConverter cannot be null");
690 |
691 | final SerializableFunction convertOrNull = filterText -> {
692 | if (filterText == null || filterText.isEmpty()) {
693 | return null;
694 | }
695 |
696 | return filterConverter.apply(filterText);
697 | };
698 |
699 | final SerializableConsumer providerFilterSlot = internalSetDataProvider(dataProvider, convertOrNull.apply(this.currentFilterText));
700 |
701 | this.filterSlot = filter -> providerFilterSlot.accept(convertOrNull.apply(filter));
702 | }
703 |
704 | @Override
705 | protected SerializableConsumer internalSetDataProvider(final DataProvider dataProvider, final F initialFilter) {
706 | final SerializableConsumer consumer = super.internalSetDataProvider(dataProvider, initialFilter);
707 |
708 | if (getDataProvider() instanceof ListDataProvider) {
709 | final ListDataProvider listDataProvider = ((ListDataProvider) getDataProvider());
710 | listDataProvider.setSortComparator((o1, o2) -> {
711 | final boolean selected1 = this.sortingSelection.contains(o1);
712 | final boolean selected2 = this.sortingSelection.contains(o2);
713 |
714 | if (selected1 && !selected2) {
715 | return -1;
716 | }
717 | if (!selected1 && selected2) {
718 | return 1;
719 | }
720 |
721 | return getItemCaptionGenerator().apply(o1)
722 | .compareToIgnoreCase(getItemCaptionGenerator().apply(o2));
723 | });
724 | }
725 |
726 | return consumer;
727 | }
728 |
729 | /**
730 | * Sets a CallbackDataProvider using the given fetch items callback and a size callback.
731 | *
732 | * This method is a shorthand for making a {@link CallbackDataProvider} that handles a partial {@link Query} object.
733 | *
734 | * @param fetchItems a callback for fetching items
735 | * @param sizeCallback a callback for getting the count of items
736 | *
737 | * @see CallbackDataProvider
738 | * @see #setDataProvider(DataProvider)
739 | */
740 | public void setDataProvider(final FetchItemsCallback fetchItems, final SerializableToIntFunction sizeCallback) {
741 | setDataProvider(new CallbackDataProvider<>(q -> fetchItems.fetchItems(q.getFilter()
742 | .orElse(""), q.getOffset(), q.getLimit()),
743 | q -> sizeCallback.applyAsInt(q.getFilter()
744 | .orElse(""))));
745 | }
746 |
747 | /**
748 | * Predicate to check {@link ComboBoxMultiselect} item captions against user typed strings.
749 | *
750 | * @see #setItems(CaptionFilter, Collection)
751 | * @see #setItems(CaptionFilter, Object[])
752 | * @since 8.0
753 | */
754 | @FunctionalInterface
755 | public interface CaptionFilter extends SerializableBiPredicate {
756 |
757 | /**
758 | * Check item caption against entered text.
759 | *
760 | * @param itemCaption the caption of the item to filter, not {@code null}
761 | * @param filterText user entered filter, not {@code null}
762 | * @return {@code true} if item passes the filter and should be listed, {@code false} otherwise
763 | */
764 | @Override
765 | public boolean test(String itemCaption, String filterText);
766 | }
767 |
768 | /**
769 | * Removes the given items. Any item that is not currently selected, is ignored. If none of the items are selected, does nothing.
770 | *
771 | * @param items the items to deselect, not {@code null}
772 | * @param userOriginated {@code true} if this was used originated, {@code false} if not
773 | */
774 | @Override
775 | protected void deselect(final Set items, final boolean userOriginated) {
776 | Objects.requireNonNull(items);
777 | if (items.stream()
778 | .noneMatch(i -> isSelected(i))) {
779 | return;
780 | }
781 |
782 | updateSelection(set -> set.removeAll(items), userOriginated, true);
783 | }
784 |
785 | /**
786 | * Deselects the given item. If the item is not currently selected, does nothing.
787 | *
788 | * @param item the item to deselect, not null
789 | * @param userOriginated {@code true} if this was used originated, {@code false} if not
790 | */
791 | @Override
792 | protected void deselect(final T item, final boolean userOriginated) {
793 | if (!getSelectedItems().contains(item)) {
794 | return;
795 | }
796 |
797 | updateSelection(set -> set.remove(item), userOriginated, false);
798 | }
799 |
800 | @Override
801 | public void deselectAll() {
802 | if (getSelectedItems().isEmpty()) {
803 | return;
804 | }
805 |
806 | updateSelection(Collection::clear, false, true);
807 | }
808 |
809 | /**
810 | * Selects the given item. Depending on the implementation, may cause other items to be deselected. If the item is already selected, does nothing.
811 | *
812 | * @param item the item to select, not null
813 | * @param userOriginated {@code true} if this was used originated, {@code false} if not
814 | */
815 | @Override
816 | protected void select(final T item, final boolean userOriginated) {
817 | if (getSelectedItems().contains(item)) {
818 | return;
819 | }
820 |
821 | updateSelection(set -> set.add(item), userOriginated, true);
822 | }
823 |
824 | @Override
825 | protected void updateSelection(final Set addedItems, final Set removedItems, final boolean userOriginated) {
826 | updateSelection(addedItems, removedItems, userOriginated, true);
827 | }
828 |
829 | /**
830 | * Updates the selection by adding and removing the given items.
831 | *
832 | * @param addedItems the items added to selection, not {@code} null
833 | * @param removedItems the items removed from selection, not {@code} null
834 | * @param userOriginated {@code true} if this was used originated, {@code false} if not
835 | * @param sortingNeeded is sorting needed before sending data back to client
836 | */
837 | protected void updateSelection(final Set addedItems, final Set removedItems, final boolean userOriginated, final boolean sortingNeeded) {
838 | Objects.requireNonNull(addedItems);
839 | Objects.requireNonNull(removedItems);
840 |
841 | // if there are duplicates, some item is both added & removed, just
842 | // discard that and leave things as was before
843 | addedItems.removeIf(item -> removedItems.remove(item));
844 |
845 | if (getSelectedItems().containsAll(addedItems) && Collections.disjoint(getSelectedItems(), removedItems)) {
846 | return;
847 | }
848 |
849 | updateSelection(set -> {
850 | // order of add / remove does not matter since no duplicates
851 | set.removeAll(removedItems);
852 | set.addAll(addedItems);
853 | }, userOriginated, sortingNeeded);
854 | }
855 |
856 | private void updateSelection(final SerializableConsumer> handler, final boolean userOriginated, final boolean sortingNeeded) {
857 | final LinkedHashSet oldSelection = new LinkedHashSet<>(getSelectedItems());
858 | final List selection = new ArrayList<>(getSelectedItems());
859 | handler.accept(selection);
860 |
861 | if (sortingNeeded) {
862 | this.sortingSelection = Collections.unmodifiableCollection(selection);
863 | }
864 |
865 | // TODO selection is private, have to use reflection (remove later)
866 | try {
867 | final Field f1 = getSelectionBaseClass().getDeclaredField("selection");
868 | f1.setAccessible(true);
869 | f1.set(this, selection);
870 | }
871 | catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
872 | e.printStackTrace();
873 | }
874 |
875 | doSetSelectedKeys(selection);
876 |
877 | fireEvent(new MultiSelectionEvent<>(this, oldSelection, userOriginated));
878 |
879 | getDataProvider().refreshAll();
880 | }
881 |
882 | protected Class> getSelectionBaseClass() {
883 | return this.getClass()
884 | .getSuperclass();
885 | }
886 |
887 | /**
888 | * Sets the selected item based on the given communication key. If the key is {@code null}, clears the current selection if any.
889 | *
890 | * @param items the selected items or {@code null} to clear selection
891 | */
892 | protected void doSetSelectedKeys(final List items) {
893 | final Set keys = itemsToKeys(items);
894 |
895 | getState().selectedItemKeys = keys;
896 |
897 | updateSelectedItemsCaption();
898 | }
899 |
900 | private void updateSelectedItemsCaption() {
901 | final List items = new ArrayList<>();
902 |
903 | if (getState().selectedItemKeys != null && !getState().selectedItemKeys.isEmpty()) {
904 | for (final String selectedItemKey : getState().selectedItemKeys) {
905 | final T value = getDataCommunicator().getKeyMapper()
906 | .get(selectedItemKey);
907 | if (value != null) {
908 | items.add(value);
909 | }
910 | }
911 | }
912 |
913 | getState().selectedItemsCaption = this.inputTextFieldCaptionGenerator.apply(items);
914 | }
915 |
916 | /**
917 | * Returns the communication key assigned to the given item.
918 | *
919 | * @param item the item whose key to return
920 | * @return the assigned key
921 | */
922 | protected String itemToKey(final T item) {
923 | if (item == null) {
924 | return null;
925 | }
926 | // TODO creates a key if item not in data provider
927 | return getDataCommunicator().getKeyMapper()
928 | .key(item);
929 | }
930 |
931 | /**
932 | * Returns the communication keys assigned to the given items.
933 | *
934 | * @param items the items whose key to return
935 | * @return the assigned keys
936 | */
937 | protected Set itemsToKeys(final List items) {
938 | if (items == null) {
939 | return null;
940 | }
941 |
942 | final Set keys = new LinkedHashSet<>();
943 | for (final T item : items) {
944 | keys.add(itemToKey(item));
945 | }
946 | return keys;
947 | }
948 |
949 | public void showClearButton(final boolean showClearButton) {
950 | getState().showClearButton = showClearButton;
951 | }
952 |
953 | public void showSelectAllButton(final boolean showSelectAllButton) {
954 | getState().showSelectAllButton = showSelectAllButton;
955 | }
956 |
957 | public void setClearButtonCaption(final String clearButtonCaption) {
958 | getState().clearButtonCaption = clearButtonCaption;
959 | }
960 |
961 | public void setSelectAllButtonCaption(final String selectAllButtonCaption) {
962 | getState().selectAllButtonCaption = selectAllButtonCaption;
963 | }
964 | }
965 |
--------------------------------------------------------------------------------
/vaadin-combobox-multiselect-addon/src/main/java/org/vaadin/addons/client/VComboBoxMultiselect.java:
--------------------------------------------------------------------------------
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 |
17 | package org.vaadin.addons.client;
18 |
19 | import java.util.ArrayList;
20 | import java.util.Arrays;
21 | import java.util.Collection;
22 | import java.util.Date;
23 | import java.util.HashSet;
24 | import java.util.Iterator;
25 | import java.util.LinkedHashSet;
26 | import java.util.List;
27 | import java.util.Set;
28 |
29 | import com.google.gwt.animation.client.AnimationScheduler;
30 | import com.google.gwt.aria.client.CheckedValue;
31 | import com.google.gwt.aria.client.Property;
32 | import com.google.gwt.aria.client.Roles;
33 | import com.google.gwt.aria.client.State;
34 | import com.google.gwt.cell.client.IsCollapsible;
35 | import com.google.gwt.core.client.JavaScriptObject;
36 | import com.google.gwt.core.client.Scheduler.ScheduledCommand;
37 | import com.google.gwt.dom.client.Document;
38 | import com.google.gwt.dom.client.Element;
39 | import com.google.gwt.dom.client.NativeEvent;
40 | import com.google.gwt.dom.client.Style;
41 | import com.google.gwt.dom.client.Style.Display;
42 | import com.google.gwt.dom.client.Style.Unit;
43 | import com.google.gwt.dom.client.Style.Visibility;
44 | import com.google.gwt.event.dom.client.BlurEvent;
45 | import com.google.gwt.event.dom.client.BlurHandler;
46 | import com.google.gwt.event.dom.client.ClickEvent;
47 | import com.google.gwt.event.dom.client.ClickHandler;
48 | import com.google.gwt.event.dom.client.FocusEvent;
49 | import com.google.gwt.event.dom.client.FocusHandler;
50 | import com.google.gwt.event.dom.client.KeyCodes;
51 | import com.google.gwt.event.dom.client.KeyDownEvent;
52 | import com.google.gwt.event.dom.client.KeyDownHandler;
53 | import com.google.gwt.event.dom.client.KeyUpEvent;
54 | import com.google.gwt.event.dom.client.KeyUpHandler;
55 | import com.google.gwt.event.dom.client.LoadEvent;
56 | import com.google.gwt.event.dom.client.LoadHandler;
57 | import com.google.gwt.event.dom.client.MouseDownEvent;
58 | import com.google.gwt.event.dom.client.MouseDownHandler;
59 | import com.google.gwt.event.logical.shared.CloseEvent;
60 | import com.google.gwt.event.logical.shared.CloseHandler;
61 | import com.google.gwt.i18n.client.HasDirection.Direction;
62 | import com.google.gwt.user.client.Command;
63 | import com.google.gwt.user.client.DOM;
64 | import com.google.gwt.user.client.Event;
65 | import com.google.gwt.user.client.Event.NativePreviewEvent;
66 | import com.google.gwt.user.client.Timer;
67 | import com.google.gwt.user.client.Window;
68 | import com.google.gwt.user.client.ui.Composite;
69 | import com.google.gwt.user.client.ui.FlowPanel;
70 | import com.google.gwt.user.client.ui.HTML;
71 | import com.google.gwt.user.client.ui.PopupPanel;
72 | import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
73 | import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
74 | import com.google.gwt.user.client.ui.TextBox;
75 | import com.google.gwt.user.client.ui.Widget;
76 | import com.vaadin.client.ApplicationConnection;
77 | import com.vaadin.client.BrowserInfo;
78 | import com.vaadin.client.ComputedStyle;
79 | import com.vaadin.client.DeferredWorker;
80 | import com.vaadin.client.Focusable;
81 | import com.vaadin.client.VConsole;
82 | import com.vaadin.client.WidgetUtil;
83 | import com.vaadin.client.ui.Field;
84 | import com.vaadin.client.ui.Icon;
85 | import com.vaadin.client.ui.SubPartAware;
86 | import com.vaadin.client.ui.VCheckBox;
87 | import com.vaadin.client.ui.VComboBox;
88 | import com.vaadin.client.ui.VLazyExecutor;
89 | import com.vaadin.client.ui.VOverlay;
90 | import com.vaadin.client.ui.aria.AriaHelper;
91 | import com.vaadin.client.ui.aria.HandlesAriaCaption;
92 | import com.vaadin.client.ui.aria.HandlesAriaInvalid;
93 | import com.vaadin.client.ui.aria.HandlesAriaRequired;
94 | import com.vaadin.client.ui.menubar.MenuBar;
95 | import com.vaadin.client.ui.menubar.MenuItem;
96 | import com.vaadin.shared.AbstractComponentState;
97 | import com.vaadin.shared.ui.ComponentStateUtil;
98 | import com.vaadin.shared.util.SharedUtil;
99 |
100 | /**
101 | * Client side implementation of the ComboBoxMultiselect component.
102 | *
103 | * TODO needs major refactoring (to be extensible etc)
104 | *
105 | * @since 8.0
106 | */
107 | @SuppressWarnings("deprecation")
108 | public class VComboBoxMultiselect extends Composite
109 | implements Field, KeyDownHandler, KeyUpHandler, ClickHandler, FocusHandler, BlurHandler, Focusable,
110 | SubPartAware, HandlesAriaCaption, HandlesAriaInvalid, HandlesAriaRequired, DeferredWorker, MouseDownHandler {
111 |
112 | /**
113 | * Represents a suggestion in the suggestion popup box.
114 | */
115 | public class ComboBoxMultiselectSuggestion implements Suggestion, Command {
116 |
117 | private final String key;
118 | private final String caption;
119 | private String untranslatedIconUri;
120 | private String style;
121 | private final VCheckBox checkBox;
122 | private Date lastExecution;
123 |
124 | /**
125 | * Constructor for a single suggestion.
126 | *
127 | * @param key
128 | * item key, empty string for a special null item not in
129 | * container
130 | * @param caption
131 | * item caption
132 | * @param style
133 | * item style name, can be empty string
134 | * @param untranslatedIconUri
135 | * icon URI or null
136 | */
137 | public ComboBoxMultiselectSuggestion(String key, String caption, String style, String untranslatedIconUri) {
138 | this.key = key;
139 | this.caption = caption;
140 | this.style = style;
141 | this.untranslatedIconUri = untranslatedIconUri;
142 |
143 | this.checkBox = new VCheckBox();
144 | this.checkBox.setEnabled(false);
145 | State.HIDDEN.set(getCheckBoxElement(), true);
146 | }
147 |
148 | /**
149 | * Gets the visible row in the popup as a HTML string. The string
150 | * contains an image tag with the rows icon (if an icon has been
151 | * specified) and the caption of the item
152 | */
153 |
154 | @Override
155 | public String getDisplayString() {
156 | final StringBuilder sb = new StringBuilder();
157 | ApplicationConnection client = VComboBoxMultiselect.this.connector.getConnection();
158 | final Icon icon = client.getIcon(client.translateVaadinUri(this.untranslatedIconUri));
159 | if (icon != null) {
160 | sb.append(icon.getElement()
161 | .getString());
162 | }
163 | String content;
164 | if ("".equals(this.caption)) {
165 | // Ensure that empty options use the same height as other
166 | // options and are not collapsed (#7506)
167 | content = " ";
168 | } else {
169 | content = WidgetUtil.escapeHTML(this.caption);
170 | }
171 | sb.append("" + content + "");
172 | return sb.toString();
173 | }
174 |
175 | /**
176 | * Get a string that represents this item. This is used in the text box.
177 | */
178 |
179 | @Override
180 | public String getReplacementString() {
181 | return this.caption;
182 | }
183 |
184 | /**
185 | * Get aria label for this item.
186 | */
187 | public String getAriaLabel() {
188 | return this.caption;
189 | }
190 |
191 | /**
192 | * Get the option key which represents the item on the server side.
193 | *
194 | * @return The key of the item
195 | */
196 | public String getOptionKey() {
197 | return this.key;
198 | }
199 |
200 | /**
201 | * Get the URI of the icon. Used when constructing the displayed option.
202 | *
203 | * @return real (translated) icon URI or null if none
204 | */
205 | public String getIconUri() {
206 | ApplicationConnection client = VComboBoxMultiselect.this.connector.getConnection();
207 | return client.translateVaadinUri(this.untranslatedIconUri);
208 | }
209 |
210 | /**
211 | * Gets the style set for this suggestion item. Styles are typically set
212 | * by a server-side. The returned style is prefixed by
213 | * v-filterselect-item-.
214 | *
215 | * @since 7.5.6
216 | * @return the style name to use, or null to not apply any
217 | * custom style.
218 | */
219 | public String getStyle() {
220 | return this.style;
221 | }
222 |
223 | /**
224 | * Executes a selection of this item.
225 | */
226 |
227 | @Override
228 | public void execute() {
229 | if (this.lastExecution == null) {
230 | this.lastExecution = new Date();
231 | onSuggestionSelected(this);
232 | return;
233 | }
234 |
235 | if (new Date().getTime() - this.lastExecution.getTime() > 300) {
236 | onSuggestionSelected(this);
237 | }
238 | }
239 |
240 | @Override
241 | public boolean equals(Object obj) {
242 | if (!(obj instanceof ComboBoxMultiselectSuggestion)) {
243 | return false;
244 | }
245 | ComboBoxMultiselectSuggestion other = (ComboBoxMultiselectSuggestion) obj;
246 | if (this.key == null && other.key != null || this.key != null && !this.key.equals(other.key)) {
247 | return false;
248 | }
249 | if (this.caption == null && other.caption != null
250 | || this.caption != null && !this.caption.equals(other.caption)) {
251 | return false;
252 | }
253 | if (!SharedUtil.equals(this.untranslatedIconUri, other.untranslatedIconUri)) {
254 | return false;
255 | }
256 |
257 | return SharedUtil.equals(this.style, other.style);
258 | }
259 |
260 | @Override
261 | public int hashCode() {
262 | final int prime = 31;
263 | int result = 1;
264 | result = prime * result + VComboBoxMultiselect.this.hashCode();
265 | result = prime * result + ((key == null) ? 0 : key.hashCode());
266 | result = prime * result + ((caption == null) ? 0 : caption.hashCode());
267 | result = prime * result + ((untranslatedIconUri == null) ? 0 : untranslatedIconUri.hashCode());
268 | result = prime * result + ((style == null) ? 0 : style.hashCode());
269 | return result;
270 | }
271 |
272 | public VCheckBox getCheckBox() {
273 | return this.checkBox;
274 | }
275 |
276 | Element getCheckBoxElement() {
277 | return this.checkBox.getElement()
278 | .getFirstChildElement();
279 | }
280 |
281 | public boolean isChecked() {
282 | return getCheckBox().getValue();
283 | }
284 |
285 | public void setChecked(boolean checked) {
286 | MenuItem menuItem = VComboBoxMultiselect.this.suggestionPopup.getMenuItem(this);
287 | if (menuItem != null) {
288 | State.CHECKED.set(menuItem.getElement(), CheckedValue.of(checked));
289 | }
290 |
291 | getCheckBox().setValue(checked);
292 | }
293 | }
294 |
295 | /** An inner class that handles all logic related to mouse wheel. */
296 | private class MouseWheeler {
297 |
298 | /**
299 | * A JavaScript function that handles the mousewheel DOM event, and
300 | * passes it on to Java code.
301 | *
302 | * @see #createMousewheelListenerFunction(Widget)
303 | */
304 | protected final JavaScriptObject mousewheelListenerFunction;
305 |
306 | protected MouseWheeler() {
307 | this.mousewheelListenerFunction = createMousewheelListenerFunction(VComboBoxMultiselect.this);
308 | }
309 |
310 | protected native JavaScriptObject createMousewheelListenerFunction(Widget widget)
311 | /*-{
312 | return $entry(function(e) {
313 | var deltaX = e.deltaX ? e.deltaX : -0.5*e.wheelDeltaX;
314 | var deltaY = e.deltaY ? e.deltaY : -0.5*e.wheelDeltaY;
315 |
316 | // IE8 has only delta y
317 | if (isNaN(deltaY)) {
318 | deltaY = -0.5*e.wheelDelta;
319 | }
320 |
321 | @org.vaadin.addons.client.VComboBoxMultiselect.JsniUtil::moveScrollFromEvent(*)(widget, deltaX, deltaY, e, e.deltaMode);
322 | });
323 | }-*/;
324 |
325 | public void attachMousewheelListener(Element element) {
326 | attachMousewheelListenerNative(element, this.mousewheelListenerFunction);
327 | }
328 |
329 | public native void attachMousewheelListenerNative(Element element, JavaScriptObject mousewheelListenerFunction)
330 | /*-{
331 | if (element.addEventListener) {
332 | // FireFox likes "wheel", while others use "mousewheel"
333 | var eventName = 'onmousewheel' in element ? 'mousewheel' : 'wheel';
334 | element.addEventListener(eventName, mousewheelListenerFunction);
335 | }
336 | }-*/;
337 |
338 | public void detachMousewheelListener(Element element) {
339 | detachMousewheelListenerNative(element, this.mousewheelListenerFunction);
340 | }
341 |
342 | public native void detachMousewheelListenerNative(Element element, JavaScriptObject mousewheelListenerFunction)
343 | /*-{
344 | if (element.addEventListener) {
345 | // FireFox likes "wheel", while others use "mousewheel"
346 | var eventName = element.onwheel===undefined?"mousewheel":"wheel";
347 | element.removeEventListener(eventName, mousewheelListenerFunction);
348 | }
349 | }-*/;
350 |
351 | }
352 |
353 | /**
354 | * A utility class that contains utility methods that are usually called
355 | * from JSNI.
356 | *
357 | * The methods are moved in this class to minimize the amount of JSNI code
358 | * as much as feasible.
359 | */
360 | static class JsniUtil {
361 | private JsniUtil() {
362 | }
363 |
364 | private static final int DOM_DELTA_PIXEL = 0;
365 | private static final int DOM_DELTA_LINE = 1;
366 | private static final int DOM_DELTA_PAGE = 2;
367 |
368 | // Rough estimation of item height
369 | private static final int SCROLL_UNIT_PX = 25;
370 |
371 | private static double deltaSum = 0;
372 |
373 | public static void moveScrollFromEvent(final Widget widget, final double deltaX, final double deltaY,
374 | final NativeEvent event, final int deltaMode) {
375 | if (!Double.isNaN(deltaY)) {
376 | VComboBoxMultiselect filterSelect = (VComboBoxMultiselect) widget;
377 |
378 | switch (deltaMode) {
379 | case DOM_DELTA_LINE:
380 | if (deltaY >= 0) {
381 | filterSelect.suggestionPopup.selectNextItem();
382 | } else {
383 | filterSelect.suggestionPopup.selectPrevItem();
384 | }
385 | break;
386 | case DOM_DELTA_PAGE:
387 | if (deltaY >= 0) {
388 | filterSelect.selectNextPage();
389 | } else {
390 | filterSelect.selectPrevPage();
391 | }
392 | break;
393 | case DOM_DELTA_PIXEL:
394 | default:
395 | // Accumulate dampened deltas
396 | deltaSum += Math.pow(Math.abs(deltaY), 0.7) * Math.signum(deltaY);
397 |
398 | // "Scroll" if change exceeds item height
399 | while (Math.abs(deltaSum) >= SCROLL_UNIT_PX) {
400 | if (!filterSelect.dataReceivedHandler.isWaitingForFilteringResponse()) {
401 | // Move selection if page flip is not in progress
402 | if (deltaSum < 0) {
403 | filterSelect.suggestionPopup.selectPrevItem();
404 | } else {
405 | filterSelect.suggestionPopup.selectNextItem();
406 | }
407 | }
408 | deltaSum -= SCROLL_UNIT_PX * Math.signum(deltaSum);
409 | }
410 | break;
411 | }
412 | }
413 | }
414 | }
415 |
416 | /**
417 | * Represents the popup box with the selection options. Wraps a suggestion
418 | * menu.
419 | */
420 | public class SuggestionPopup extends VOverlay implements PositionCallback, CloseHandler {
421 |
422 | private static final int Z_INDEX = 30000;
423 |
424 | /** For internal use only. May be removed or replaced in the future. */
425 | public final SuggestionMenu menu;
426 |
427 | private final Element up = DOM.createDiv();
428 | private final Element down = DOM.createDiv();
429 | private final Element status = DOM.createDiv();
430 |
431 | private boolean isPagingEnabled = true;
432 |
433 | private long lastAutoClosed;
434 |
435 | private int popupOuterPadding = -1;
436 |
437 | private int topPosition;
438 | private int leftPosition;
439 |
440 | private final MouseWheeler mouseWheeler = new MouseWheeler();
441 |
442 | private boolean scrollPending = false;
443 |
444 | /**
445 | * Default constructor
446 | */
447 | SuggestionPopup() {
448 | super(true, false);
449 | debug("VComboBoxMultiselect.SP: constructor()");
450 | setOwner(VComboBoxMultiselect.this);
451 | this.menu = new SuggestionMenu();
452 | setWidget(this.menu);
453 |
454 | getElement().getStyle()
455 | .setZIndex(Z_INDEX);
456 |
457 | final Element root = getContainerElement();
458 |
459 | this.up.setInnerHTML("Prev");
460 | DOM.sinkEvents(this.up, Event.ONCLICK);
461 |
462 | this.down.setInnerHTML("Next");
463 | DOM.sinkEvents(this.down, Event.ONCLICK);
464 |
465 | root.insertFirst(this.up);
466 | root.appendChild(this.down);
467 | root.appendChild(this.status);
468 |
469 | DOM.sinkEvents(root, Event.ONMOUSEDOWN | Event.ONMOUSEWHEEL);
470 | addCloseHandler(this);
471 |
472 | Roles.getListRole()
473 | .set(getElement());
474 |
475 | setPreviewingAllNativeEvents(true);
476 | }
477 |
478 | public MenuItem getMenuItem(Command command) {
479 | for (MenuItem menuItem : this.menu.getItems()) {
480 | if (command.equals(menuItem.getCommand())) {
481 | return menuItem;
482 | }
483 | }
484 | return null;
485 | }
486 |
487 | @Override
488 | protected void onLoad() {
489 | super.onLoad();
490 |
491 | // Register mousewheel listener on paged select
492 | if (VComboBoxMultiselect.this.pageLength > 0) {
493 | this.mouseWheeler.attachMousewheelListener(getElement());
494 | }
495 | }
496 |
497 | @Override
498 | protected void onUnload() {
499 | this.mouseWheeler.detachMousewheelListener(getElement());
500 | super.onUnload();
501 | }
502 |
503 | /**
504 | * Shows the popup where the user can see the filtered options that have
505 | * been set with a call to
506 | * {@link SuggestionMenu#setSuggestions(Collection)}.
507 | *
508 | * @param currentPage
509 | * The current page number
510 | */
511 | public void showSuggestions(final int currentPage) {
512 | debug("VComboBoxMultiselect.SP: showSuggestions(" + currentPage + ", " + getTotalSuggestions() + ")");
513 |
514 | final SuggestionPopup popup = this;
515 | // Add TT anchor point
516 | getElement().setId("VAADIN_COMBOBOX_OPTIONLIST");
517 |
518 | this.leftPosition = getDesiredLeftPosition();
519 | this.topPosition = getDesiredTopPosition();
520 |
521 | setPopupPosition(this.leftPosition, this.topPosition);
522 |
523 | final int first = currentPage * VComboBoxMultiselect.this.pageLength + 1;
524 | final int last = first + VComboBoxMultiselect.this.currentSuggestions.size() - 1;
525 | final int matches = getTotalSuggestions();
526 | if (last > 0) {
527 | // nullsel not counted, as requested by user
528 | this.status.setInnerText((matches == 0 ? 0 : first) + "-" + last + "/" + matches);
529 | } else {
530 | this.status.setInnerText("");
531 | }
532 | // We don't need to show arrows or statusbar if there is
533 | // only one page
534 | if (matches <= VComboBoxMultiselect.this.pageLength || VComboBoxMultiselect.this.pageLength == 0) {
535 | setPagingEnabled(false);
536 | } else {
537 | setPagingEnabled(true);
538 | }
539 | setPrevButtonActive(first > 1);
540 | setNextButtonActive(last < matches);
541 |
542 | // clear previously fixed width
543 | this.menu.setWidth("");
544 | this.menu.getElement()
545 | .getFirstChildElement()
546 | .getStyle()
547 | .clearWidth();
548 |
549 | setPopupPositionAndShow(popup);
550 | }
551 |
552 | private int getDesiredTopPosition() {
553 | return toInt32(WidgetUtil.getBoundingClientRect(VComboBoxMultiselect.this.tb.getElement())
554 | .getBottom()) + Window.getScrollTop();
555 | }
556 |
557 | private int getDesiredLeftPosition() {
558 | return toInt32(WidgetUtil.getBoundingClientRect(VComboBoxMultiselect.this.getElement())
559 | .getLeft());
560 | }
561 |
562 | private native int toInt32(double val)
563 | /*-{
564 | return val | 0;
565 | }-*/;
566 |
567 | /**
568 | * Should the next page button be visible to the user?
569 | *
570 | * @param active
571 | */
572 | private void setNextButtonActive(boolean active) {
573 | debug("VComboBoxMultiselect.SP: setNextButtonActive(" + active + ")");
574 |
575 | if (active) {
576 | DOM.sinkEvents(this.down, Event.ONCLICK);
577 | this.down.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-nextpage");
578 | } else {
579 | DOM.sinkEvents(this.down, 0);
580 | this.down.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-nextpage-off");
581 | }
582 | }
583 |
584 | /**
585 | * Should the previous page button be visible to the user
586 | *
587 | * @param active
588 | */
589 | private void setPrevButtonActive(boolean active) {
590 | debug("VComboBoxMultiselect.SP: setPrevButtonActive(" + active + ")");
591 |
592 | if (active) {
593 | DOM.sinkEvents(this.up, Event.ONCLICK);
594 | this.up.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-prevpage");
595 | } else {
596 | DOM.sinkEvents(this.up, 0);
597 | this.up.setClassName(VComboBoxMultiselect.this.getStylePrimaryName() + "-prevpage-off");
598 | }
599 |
600 | }
601 |
602 | /**
603 | * Selects the next item in the filtered selections.
604 | */
605 | public void selectNextItem() {
606 | debug("VComboBoxMultiselect.SP: selectNextItem()");
607 |
608 | final int index = this.menu.getSelectedIndex() + 1;
609 | if (this.menu.getItems()
610 | .size() > index) {
611 | selectItem(this.menu.getItems()
612 | .get(index));
613 |
614 | } else {
615 | selectNextPage();
616 | }
617 | }
618 |
619 | /**
620 | * Selects the previous item in the filtered selections.
621 | */
622 | public void selectPrevItem() {
623 | debug("VComboBoxMultiselect.SP: selectPrevItem()");
624 |
625 | final int index = this.menu.getSelectedIndex() - 1;
626 | if (index > -1) {
627 | selectItem(this.menu.getItems()
628 | .get(index));
629 |
630 | } else if (index == -1) {
631 | selectPrevPage();
632 |
633 | } else {
634 | if (!this.menu.getItems()
635 | .isEmpty()) {
636 | selectLastItem();
637 | }
638 | }
639 | }
640 |
641 | /**
642 | * Select the first item of the suggestions list popup.
643 | *
644 | * @since 7.2.6
645 | */
646 | public void selectFirstItem() {
647 | debug("VFS.SP: selectFirstItem()");
648 | int index = 0;
649 | List